Compare commits
180 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3f7816762e | ||
|
|
8bade51eb5 | ||
|
|
7c4398bfb5 | ||
|
|
8f736e8bd3 | ||
|
|
65a9b11dc5 | ||
|
|
7ffd16df4b | ||
|
|
e2628d27dc | ||
|
|
8799c2bb5e | ||
|
|
4ba0b64d2c | ||
|
|
4b2f43e8a2 | ||
|
|
df774ca3c5 | ||
|
|
084fab576e | ||
|
|
1733e64403 | ||
|
|
1d0360c0c3 | ||
|
|
45823ccd96 | ||
|
|
ef3b64cf51 | ||
|
|
a424fb8793 | ||
|
|
b6ae819d32 | ||
|
|
8711ae2452 | ||
|
|
a90c2c2fa8 | ||
|
|
03a89d4f43 | ||
|
|
d48b2bdf2a | ||
|
|
6a6ca8c642 | ||
|
|
d3ffecb866 | ||
|
|
083f91611a | ||
|
|
01e9c45df6 | ||
|
|
b327ebc5bd | ||
|
|
ec1a5add73 | ||
|
|
b7110a7222 | ||
|
|
2e19d2eac1 | ||
|
|
ad7f388d68 | ||
|
|
f28c1e7fae | ||
|
|
6ce342c0b3 | ||
|
|
622bdf613c | ||
|
|
a0efd1087f | ||
|
|
9813295fd3 | ||
|
|
303fdfc9ad | ||
|
|
90d88a998c | ||
|
|
1999e1daf2 | ||
|
|
433f39dd38 | ||
|
|
110746e859 | ||
|
|
640860a3af | ||
|
|
b21fa807db | ||
|
|
446dd921bf | ||
|
|
594fccd602 | ||
|
|
47993b612f | ||
|
|
2302f2dbef |
70
.config/generate_xml.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def indent(elem, level=0):
|
||||
'''
|
||||
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
|
||||
|
||||
|
||||
try:
|
||||
py_version = sys.argv[1]
|
||||
except IndexError:
|
||||
print('No version specified')
|
||||
sys.exit(1)
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# Load template file
|
||||
with open('{dir_path}/template.xml'.format(**locals()), 'r') as f:
|
||||
tree = ET.parse(f)
|
||||
root = tree.getroot()
|
||||
|
||||
# Load version dependencies
|
||||
with open('{dir_path}/{py_version}.yaml'.format(**locals()), 'r') as f:
|
||||
deps = yaml.safe_load(f)
|
||||
|
||||
# Load version and changelog
|
||||
with open('jellyfin-kodi/release.yaml', 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# Populate xml template
|
||||
for dep in deps:
|
||||
ET.SubElement(root.find('requires'), 'import', attrib=dep)
|
||||
|
||||
# Update version string
|
||||
addon_version = data.get('version')
|
||||
root.attrib['version'] = '{addon_version}+{py_version}'.format(**locals())
|
||||
|
||||
# Changelog
|
||||
date = datetime.today().strftime('%Y-%m-%d')
|
||||
changelog = data.get('changelog')
|
||||
for section in root.findall('extension'):
|
||||
news = section.findall('news')
|
||||
if news:
|
||||
news[0].text = 'v{addon_version} ({date}):\n{changelog}'.format(**locals())
|
||||
|
||||
# Format xml tree
|
||||
indent(root)
|
||||
|
||||
# Write addon.xml
|
||||
tree.write('jellyfin-kodi/addon.xml', encoding='utf-8', xml_declaration=True)
|
||||
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.jellycon"
|
||||
name="JellyCon"
|
||||
version="0.1.1"
|
||||
provider-name="Team B">
|
||||
version=""
|
||||
provider-name="Jellyfin Contributors">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.25.0"/>
|
||||
<import addon="script.module.pil" version="1.1.7"/>
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||
<provides>video audio</provides>
|
||||
@@ -20,7 +18,7 @@
|
||||
<website>https://github.com/jellyfin/jellycon/wiki</website>
|
||||
<source>https://github.com/jellyfin/jellycon</source>
|
||||
<summary lang="en_GB">Browse and play your Jellyfin server media library.</summary>
|
||||
<description lang="en_GB">An addon to allow you to browse and playback your Jellyfin (www.jellyfin.org) Movie, TV Show and Music collections.</description>
|
||||
<description lang="en_GB">An addon to allow you to browse and playback your Jellyfin (https://jellyfin.org) Movie, TV Show and Music collections.</description>
|
||||
<assets>
|
||||
<icon>icon.png</icon>
|
||||
<fanart>fanart.jpg</fanart>
|
||||
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. Android, Debian, Windows]
|
||||
- Jellyfin Version: [e.g. 10.0.1]
|
||||
- Kodi Version: [e.g. 18.3]
|
||||
- Addon Version: [e.g. 0.1.1]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
16
.github/dependabot.yaml
vendored
Normal file
@@ -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(), use_aliases=True, variant="emoji_type")
|
||||
for x in sys.stdin.readlines()
|
||||
if x.strip()
|
||||
]
|
||||
|
||||
sections = []
|
||||
|
||||
section: Union[SectionType, Dict] = {}
|
||||
for line in data:
|
||||
if line.startswith("## "):
|
||||
pass
|
||||
if line.startswith("### "):
|
||||
if section:
|
||||
sections.append(section)
|
||||
_section: SectionType = {
|
||||
"title": line.strip("# "),
|
||||
"items": [],
|
||||
}
|
||||
section = _section
|
||||
|
||||
m = ITEM_PATTERN.match(line)
|
||||
if m:
|
||||
gd = m.groupdict()
|
||||
section["items"].append(gd)
|
||||
|
||||
sections.append(section)
|
||||
|
||||
first = True
|
||||
|
||||
for section in sections:
|
||||
if not section:
|
||||
continue
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
print()
|
||||
|
||||
title = section["title"]
|
||||
if not output_emoji:
|
||||
title = replace_emoji(title).strip()
|
||||
|
||||
print(title)
|
||||
print("-" * len(title))
|
||||
|
||||
for item in section["items"]:
|
||||
formatted_item = item_format.format(**item)
|
||||
if not output_emoji:
|
||||
formatted_item = demojize(formatted_item)
|
||||
print(formatted_item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--format", type=str, default=ITEM_FORMAT)
|
||||
|
||||
parser.add_argument("--no-emoji", dest="emoji", action="store_false")
|
||||
parser.add_argument("--emoji", dest="emoji", action="store_true")
|
||||
parser.set_defaults(emoji=OUTPUT_EMOJI)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
reformat(args.format, args.emoji)
|
||||
39
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Build JellyCon
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
py_version: [ 'py2', 'py3' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pyyaml
|
||||
|
||||
- name: Create ${{ matrix.py_version }} addon.xml
|
||||
run: python build.py --version ${{ matrix.py_version }}
|
||||
|
||||
- name: Publish Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
retention-days: 14
|
||||
name: ${{ matrix.py_version }}-build-artifact
|
||||
path: |
|
||||
*.zip
|
||||
41
.github/workflows/codeql.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: CodeQL Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '38 8 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellycon' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
version: ['2.7', '3.9']
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-and-quality
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.version }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
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.15.0
|
||||
id: draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Setup YQ
|
||||
uses: chrisdickinson/setup-yq@latest
|
||||
with:
|
||||
yq-version: v4.9.1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Parse Changlog
|
||||
run: |
|
||||
pip install emoji
|
||||
cat << EOF >> cl.md
|
||||
${{ steps.draft.outputs.body }}
|
||||
EOF
|
||||
TAG="${{ steps.draft.outputs.tag_name }}"
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
echo "YAML_CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
cat cl.md | python .github/tools/reformat_changelog.py --no-emoji >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
cat cl.md | python .github/tools/reformat_changelog.py --emoji --format='+ #{issue} by @{username}' >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
rm cl.md
|
||||
|
||||
- name: Update release.yaml
|
||||
run: |
|
||||
yq eval '.version = env(VERSION) | .changelog = strenv(YAML_CHANGELOG) | .changelog style="literal"' -i release.yaml
|
||||
|
||||
- name: Commit Changes
|
||||
run: |
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
|
||||
git checkout -b prepare-${{ env.VERSION }}
|
||||
git commit -am "bump version to ${{ env.VERSION }}"
|
||||
|
||||
if [[ -z "$(git ls-remote --heads origin prepare-${{ env.VERSION }})" ]]; then
|
||||
git push origin prepare-${{ env.VERSION }}
|
||||
else
|
||||
git push -f origin prepare-${{ env.VERSION }}
|
||||
fi
|
||||
|
||||
- name: Create or Update PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
pr_title: Prepare for release ${{ steps.draft.outputs.tag_name }}
|
||||
pr_source: prepare-${{ env.VERSION }}
|
||||
pr_labels: 'release-prep,skip-changelog'
|
||||
pr_body: |
|
||||
:robot: This is a generated PR to bump the `release.yaml` version and update the changelog.
|
||||
|
||||
---
|
||||
|
||||
${{ env.CHANGELOG }}
|
||||
64
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Publish JellyCon
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
py_version: [ 'py2', 'py3' ]
|
||||
steps:
|
||||
- name: Update Draft
|
||||
uses: release-drafter/release-drafter@v5.15.0
|
||||
if: ${{ matrix.py_version == 'py3' }}
|
||||
with:
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pyyaml
|
||||
|
||||
- name: Create ${{ matrix.py_version }} addon.xml
|
||||
run: python build.py --version ${{ matrix.py_version }}
|
||||
|
||||
- name: Publish Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
retention-days: 14
|
||||
name: ${{ matrix.py_version }}-build-artifact
|
||||
path: |
|
||||
*.zip
|
||||
|
||||
- name: Upload to repo server
|
||||
uses: burnett01/rsync-deployments@5.1
|
||||
with:
|
||||
switches: -vrptz
|
||||
path: '*.zip'
|
||||
remote_path: /srv/repository/incoming/kodi
|
||||
remote_host: ${{ secrets.DEPLOY_HOST }}
|
||||
remote_user: ${{ secrets.DEPLOY_USER }}
|
||||
remote_key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
- name: Add to Kodi repo and clean up
|
||||
uses: appleboy/ssh-action@v0.1.4
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
script_stop: true
|
||||
script: |
|
||||
python3 /usr/local/bin/kodirepo add /srv/repository/incoming/kodi/plugin.video.jellycon+${{ matrix.py_version }}.zip --datadir /srv/repository/releases/client/kodi/${{ matrix.py_version }};
|
||||
rm /srv/repository/incoming/kodi/plugin.video.jellycon+${{ matrix.py_version }}.zip;
|
||||
16
.github/workflows/release-drafter.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
name: Update release draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update Release Draft
|
||||
uses: release-drafter/release-drafter@v5.15.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
49
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Test JellyCon
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
PR_TRIGGERED: ${{ github.event_name == 'pull_request' && github.repository == 'jellyfin/jellycon' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
py_version: ['2.7', '3.9']
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.py_version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.py_version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements-dev.txt
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --output-file=flake8.output
|
||||
cat flake8.output
|
||||
|
||||
- name: Publish Test Atrifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
retention-days: 14
|
||||
name: ${{ matrix.py_version }}-test-results
|
||||
path: |
|
||||
flake8.output
|
||||
3
.gitignore
vendored
@@ -220,3 +220,6 @@ pip-log.txt
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
# Addon files
|
||||
addon.xml
|
||||
|
||||
108
build.py
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def indent(elem, level=0):
|
||||
'''
|
||||
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, source, py_version):
|
||||
'''
|
||||
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, source, target):
|
||||
'''
|
||||
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 files:
|
||||
if 'plugin.video.jellycon' not in filename and 'pyo' not in filename:
|
||||
file_path = os.path.join(root, filename)
|
||||
relative_path = os.path.join('plugin.video.jellycon', os.path.relpath(file_path, source))
|
||||
z.write(file_path, relative_path)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load config file
|
||||
config_path = os.path.join(args.source, 'release.yaml')
|
||||
with open(config_path, 'r') as fh:
|
||||
config = yaml.safe_load(fh)
|
||||
|
||||
create_addon_xml(config, args.source, args.version)
|
||||
|
||||
zip_files(args.version, args.source, args.target)
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import xbmcaddon
|
||||
|
||||
from resources.lib.simple_logging import SimpleLogging
|
||||
from resources.lib.loghandler import LazyLogger
|
||||
from resources.lib.functions import main_entry_point
|
||||
from resources.lib.tracking import set_timing_enabled
|
||||
|
||||
log = SimpleLogging('default')
|
||||
log = LazyLogger('default')
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
log_timing_data = settings.getSetting('log_timing') == "true"
|
||||
|
||||
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.4.5'
|
||||
changelog: |-
|
||||
New features and improvements
|
||||
-----------------------------
|
||||
+ Attempt to reestablish websocket connection if it fails (#93) @mcarlton00
|
||||
+ Combine NextUp and InProgress (#82) @Ozymandyaz
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
+ Report tracks correctly when a playlist is playing (#102) @mcarlton00
|
||||
+ Fix image caching (#101) @mcarlton00
|
||||
+ Properly report stopped playback to the server (#95) @mcarlton00
|
||||
+ Make API calls respect limits indicated in UI (#90) @mcarlton00
|
||||
+ Fix #87 (#88) @oddstr13
|
||||
+ Fix direct play logic for 10.8 (#84) @mcarlton00
|
||||
|
||||
Code or Repo Maintenance
|
||||
------------------------
|
||||
+ Move inprogress call into relevant if block (#91) @mcarlton00
|
||||
+ Disable screensaver settings by default (#85) @mcarlton00
|
||||
|
||||
CI & build changes
|
||||
------------------
|
||||
+ Ci dependencies (#100) @mcarlton00
|
||||
+ Migrate CI to github actions (#96) @mcarlton00
|
||||
+ Correct addon name in build.py (#89) @mcarlton00
|
||||
dependencies:
|
||||
py2:
|
||||
- addon: 'xbmc.python'
|
||||
version: '2.25.0'
|
||||
- addon: 'script.module.requests'
|
||||
version: '2.22.0'
|
||||
- addon: 'script.module.dateutil'
|
||||
version: '2.8.1'
|
||||
- addon: 'script.module.six'
|
||||
version: '1.13.0'
|
||||
- addon: 'script.module.kodi-six'
|
||||
version: '0.0.7'
|
||||
- addon: 'script.module.addon.signals'
|
||||
version: '0.0.5'
|
||||
- addon: 'script.module.futures'
|
||||
version: '2.2.0'
|
||||
- addon: 'script.module.websocket'
|
||||
version: '0.57.0'
|
||||
py3:
|
||||
- addon: 'xbmc.python'
|
||||
version: '3.0.0'
|
||||
- addon: 'script.module.requests'
|
||||
version: '2.22.0+matrix.1'
|
||||
- addon: 'script.module.dateutil'
|
||||
version: '2.8.1+matrix.1'
|
||||
- addon: 'script.module.six'
|
||||
version: '1.14.0+matrix.2'
|
||||
- addon: 'script.module.kodi-six'
|
||||
version: '0.1.3+1'
|
||||
- addon: 'script.module.addon.signals'
|
||||
version: '0.0.5+matrix.1'
|
||||
- addon: 'script.module.websocket'
|
||||
version: '0.57.0+matrix.1'
|
||||
16
requirements-dev.txt
Normal file
@@ -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
|
||||
2
resources/language/resource.language.ar/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.cy/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"
|
||||
149
resources/language/resource.language.de/strings.po
Normal file
@@ -0,0 +1,149 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2021-11-12 19:59+0000\n"
|
||||
"Last-Translator: Moritz <moritz.leick@googlemail.com>\n"
|
||||
"Language-Team: German <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/de/>\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.5.2\n"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "Anzeige des Ladefortschritts"
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "Sekunden zurückspringen"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "Abrufen von Daten"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "Laden von Inhalten"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "Dienste"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "Schnittstelle"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr ""
|
||||
"Warnung: Bei dieser Aktion werden die Mediendateien vom Server gelöscht."
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "Bestätigen Sie das Löschen?"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "Warten auf Server zum Löschen"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "Löschen"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "Benutzername nicht gefunden"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "Falscher Benutzername/Passwort"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Debug-Protokollierung einschalten"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Passwort:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Benutzername:"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "Details zu nicht gesehenen Episoden ausblenden"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Erweiterte"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "Einzelne Staffel reduzieren"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Gefiltertes Format für Episodennamen"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Anzahl der Elemente, die in gefilterten Listen angezeigt werden sollen"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Verbundene Clients anzeigen"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Anzeigename des Geräts"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "Zeitdaten protokollieren"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Benutzer wechseln]"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Lokalen Server ermitteln]"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "Anzahl der zu erfassenden Leistungsprofile"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Samba-Passwort"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Samba-Benutzername"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "HTTPS-Zertifikat prüfen"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Host"
|
||||
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"
|
||||
1087
resources/language/resource.language.eo/strings.po
Normal file
988
resources/language/resource.language.es/strings.po
Normal file
@@ -0,0 +1,988 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2021-12-07 18:05+0000\n"
|
||||
"Last-Translator: oxixes <adrianquevedobenito@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/es/>\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.5.2\n"
|
||||
|
||||
msgctxt "#30442"
|
||||
msgid "Simple new content check"
|
||||
msgstr "Comprobación simple de nuevo contenido"
|
||||
|
||||
msgctxt "#30440"
|
||||
msgid "Play next"
|
||||
msgstr "Reproducir siguiente"
|
||||
|
||||
msgctxt "#30439"
|
||||
msgid "Show play next episode at time left"
|
||||
msgstr "Mostrar reproducir siguiente episodio al tiempo restante"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30438"
|
||||
msgid "Play cinema intros"
|
||||
msgstr "Reproducir introducciones de cine"
|
||||
|
||||
msgctxt "#30437"
|
||||
msgid "Playback options"
|
||||
msgstr "Opciones de reproducción"
|
||||
|
||||
msgctxt "#30436"
|
||||
msgid "Speed test data size (MB)"
|
||||
msgstr "Tamaño de los datos de la prueba de velocidad (MB)"
|
||||
|
||||
msgctxt "#30435"
|
||||
msgid "Connection speed test"
|
||||
msgstr "Prueba de velocidad de conexión"
|
||||
|
||||
msgctxt "#30434"
|
||||
msgid "Force transcode stream bitrate (Kbits)"
|
||||
msgstr "Tasa de bits de la transcodificación forzada (Kbps)"
|
||||
|
||||
msgctxt "#30433"
|
||||
msgid "Allow direct file playback"
|
||||
msgstr "Permitir reproducción directa de archivo"
|
||||
|
||||
msgctxt "#30432"
|
||||
msgid "Hide watched items in lists"
|
||||
msgstr "Esconder elementos vistos en las listas"
|
||||
|
||||
msgctxt "#30431"
|
||||
msgid "Seasons"
|
||||
msgstr "Temporadas"
|
||||
|
||||
msgctxt "#30430"
|
||||
msgid "Label"
|
||||
msgstr "Etiqueta"
|
||||
|
||||
msgctxt "#30429"
|
||||
msgid "Genre"
|
||||
msgstr "Género"
|
||||
|
||||
msgctxt "#30428"
|
||||
msgid "Rating"
|
||||
msgstr "Calificación"
|
||||
|
||||
msgctxt "#30427"
|
||||
msgid "Added"
|
||||
msgstr "Añadido"
|
||||
|
||||
msgctxt "#30426"
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
msgctxt "#30425"
|
||||
msgid "Year"
|
||||
msgstr "Año"
|
||||
|
||||
msgctxt "#30424"
|
||||
msgid "Default"
|
||||
msgstr "Por defecto"
|
||||
|
||||
msgctxt "#30422"
|
||||
msgid "Sorting"
|
||||
msgstr "Ordenado"
|
||||
|
||||
msgctxt "#30420"
|
||||
msgid "Audio max channels"
|
||||
msgstr "Máximos canales de audio"
|
||||
|
||||
msgctxt "#30419"
|
||||
msgid "Audio codec"
|
||||
msgstr "Códec de audio"
|
||||
|
||||
msgctxt "#30418"
|
||||
msgid "Audio bitrate (Kbits)"
|
||||
msgstr "Tasa de bits de audio (Kbps)"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid "You do not have permision to delete this item"
|
||||
msgstr "No tienes permiso para borrar este elemento"
|
||||
|
||||
msgctxt "#30416"
|
||||
msgid "HTTP timeout seconds"
|
||||
msgstr "Segundos del tiempo de espera de HTTP"
|
||||
|
||||
msgctxt "#30415"
|
||||
msgid " - Favorite Collections"
|
||||
msgstr "- Colecciones favoritas"
|
||||
|
||||
msgctxt "#30414"
|
||||
msgid " - Favorites"
|
||||
msgstr "- Favoritos"
|
||||
|
||||
msgctxt "#30413"
|
||||
msgid " - Tags"
|
||||
msgstr "- Etiquetas"
|
||||
|
||||
msgctxt "#30412"
|
||||
msgid " - Decades"
|
||||
msgstr "- Décadas"
|
||||
|
||||
msgctxt "#30411"
|
||||
msgid " - Years"
|
||||
msgstr "- Años"
|
||||
|
||||
msgctxt "#30410"
|
||||
msgid " - Collections"
|
||||
msgstr "- Colecciones"
|
||||
|
||||
msgctxt "#30409"
|
||||
msgid "Add-on Actions"
|
||||
msgstr "Acciones del complemento"
|
||||
|
||||
msgctxt "#30407"
|
||||
msgid "Global Lists"
|
||||
msgstr "Listas globales"
|
||||
|
||||
msgctxt "#30406"
|
||||
msgid "Jellyfin Libraries"
|
||||
msgstr "Bibliotecas de Jellyfin"
|
||||
|
||||
msgctxt "#30405"
|
||||
msgid " - Show All"
|
||||
msgstr "- Mostrar todo"
|
||||
|
||||
msgctxt "#30404"
|
||||
msgid " - A-Z"
|
||||
msgstr "- A-Z"
|
||||
|
||||
msgctxt "#30403"
|
||||
msgid "Movies - Recommendations"
|
||||
msgstr "Películas - Recomendaciones"
|
||||
|
||||
msgctxt "#30402"
|
||||
msgid "Add to Kodi Playlist"
|
||||
msgstr "Añadir a la lista de reproducción de Kodi"
|
||||
|
||||
msgctxt "#30401"
|
||||
msgid "Info"
|
||||
msgstr "Información"
|
||||
|
||||
msgctxt "#30399"
|
||||
msgid "Hide"
|
||||
msgstr "Esconder"
|
||||
|
||||
msgctxt "#30398"
|
||||
msgid "Refresh Jellyfin Metadata"
|
||||
msgstr "Recargar metadatos de Jellyfin"
|
||||
|
||||
msgctxt "#30397"
|
||||
msgid " - Pages"
|
||||
msgstr "- Páginas"
|
||||
|
||||
msgctxt "#30395"
|
||||
msgid "Clear cached server data"
|
||||
msgstr "Borrar datos del servidor en caché"
|
||||
|
||||
msgctxt "#30394"
|
||||
msgid "Cache files deleted"
|
||||
msgstr "Archivos del caché borrados"
|
||||
|
||||
msgctxt "#30393"
|
||||
msgid "Clear Cache Result"
|
||||
msgstr "Borrar caché del resultado"
|
||||
|
||||
msgctxt "#30392"
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgctxt "#30391"
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgctxt "#30390"
|
||||
msgid "Protocol"
|
||||
msgstr "Protocolo"
|
||||
|
||||
msgctxt "#30389"
|
||||
msgid "User details"
|
||||
msgstr "Detalles del usuario"
|
||||
|
||||
msgctxt "#30388"
|
||||
msgid "Server details"
|
||||
msgstr "Detalles del servidor"
|
||||
|
||||
msgctxt "#30387"
|
||||
msgid "Unused images removed : "
|
||||
msgstr "Imágenes no usadas eliminadas:"
|
||||
|
||||
msgctxt "#30386"
|
||||
msgid "Unused Jellyfin images : "
|
||||
msgstr "Imágenes de Jellyfin no usadas:"
|
||||
|
||||
msgctxt "#30385"
|
||||
msgid "Existing images before delete : "
|
||||
msgstr "Imágenes existentes antes de borrar:"
|
||||
|
||||
msgctxt "#30383"
|
||||
msgid "System - "
|
||||
msgstr "Sistema -"
|
||||
|
||||
msgctxt "#30382"
|
||||
msgid "Always"
|
||||
msgstr "Siempre"
|
||||
|
||||
msgctxt "#30381"
|
||||
msgid "More than one"
|
||||
msgstr "Más de una"
|
||||
|
||||
msgctxt "#30380"
|
||||
msgid "Never"
|
||||
msgstr "Nunca"
|
||||
|
||||
msgctxt "#30378"
|
||||
msgid "Persist user details"
|
||||
msgstr "Conservar detalles del usuario"
|
||||
|
||||
msgctxt "#30377"
|
||||
msgid "Sending request"
|
||||
msgstr "Enviando petición"
|
||||
|
||||
msgctxt "#30376"
|
||||
msgid "Checking server url"
|
||||
msgstr "Comprobando la URL del servidor"
|
||||
|
||||
msgctxt "#30375"
|
||||
msgid "Receiving data packet"
|
||||
msgstr "Recibiendo paquete de datos"
|
||||
|
||||
msgctxt "#30374"
|
||||
msgid "Sending request"
|
||||
msgstr "Enviando petición"
|
||||
|
||||
msgctxt "#30373"
|
||||
msgid "Scanning for local servers"
|
||||
msgstr "Escaneando servidores locales"
|
||||
|
||||
msgctxt "#30372"
|
||||
msgid "Server URL"
|
||||
msgstr "URL del servidor"
|
||||
|
||||
msgctxt "#30371"
|
||||
msgid "Could not connect to the URL you entered, do you want to try again?"
|
||||
msgstr ""
|
||||
"No se ha podido conectar a la URL introducida, ¿quieres intentarlo otra vez?"
|
||||
|
||||
msgctxt "#30370"
|
||||
msgid "Do you want to manually enter a server url?"
|
||||
msgstr "¿Quieres introducir una URL de servidor manualmente?"
|
||||
|
||||
msgctxt "#30369"
|
||||
msgid "Do you want to clear your saved password?"
|
||||
msgstr "¿Quieres borrar tu contraseña guardada?"
|
||||
|
||||
msgctxt "#30368"
|
||||
msgid "Clear Password?"
|
||||
msgstr "¿Borrar contraseña?"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30367"
|
||||
msgid "Allow fast user switching password saving"
|
||||
msgstr "Permitir guardado rápido de la contraseña de usuario cambiada"
|
||||
|
||||
msgctxt "#30366"
|
||||
msgid "Manually enter user details"
|
||||
msgstr "Introduce los detalles del usuario manualmente"
|
||||
|
||||
msgctxt "#30365"
|
||||
msgid "Manual Login"
|
||||
msgstr "Inicio de sesión manual"
|
||||
|
||||
msgctxt "#30364"
|
||||
msgid "Do you want to save the password?"
|
||||
msgstr "¿Quieres guardar la contraseña?"
|
||||
|
||||
msgctxt "#30363"
|
||||
msgid "Save Password?"
|
||||
msgstr "¿Guardar contraseña?"
|
||||
|
||||
msgctxt "#30362"
|
||||
msgid " - Recordings"
|
||||
msgstr "- Grabaciones"
|
||||
|
||||
msgctxt "#30361"
|
||||
msgid " - Programs"
|
||||
msgstr "- Programas"
|
||||
|
||||
msgctxt "#30360"
|
||||
msgid " - Channels"
|
||||
msgstr "- Canales"
|
||||
|
||||
msgctxt "#30359"
|
||||
msgid "Building full image list"
|
||||
msgstr "Construyendo la lista de imágenes completa"
|
||||
|
||||
msgctxt "#30358"
|
||||
msgid "Retreiving remote image list"
|
||||
msgstr "Recuperando la lista de imágenes remota"
|
||||
|
||||
msgctxt "#30357"
|
||||
msgid "Processing existing image list"
|
||||
msgstr "Procesando la lista de imágenes existentes"
|
||||
|
||||
msgctxt "#30356"
|
||||
msgid "Loading existing image list"
|
||||
msgstr "Cargando lista de imágenes existentes"
|
||||
|
||||
msgctxt "#30355"
|
||||
msgid "Kodi Settings->Services->Allow remote control via HTTP"
|
||||
msgstr ""
|
||||
"Configuración de Kodi -> Servicios -> Permitir control remoto mediante HTTP"
|
||||
|
||||
msgctxt "#30354"
|
||||
msgid "Go To Series"
|
||||
msgstr "Ir a las series"
|
||||
|
||||
msgctxt "#30353"
|
||||
msgid " - Frequently Played"
|
||||
msgstr "- Reproducido frecuentemente"
|
||||
|
||||
msgctxt "#30321"
|
||||
msgid " - Album Artists"
|
||||
msgstr "- Artistas del álbum"
|
||||
|
||||
msgctxt "#30319"
|
||||
msgid "Music - All Album Artists"
|
||||
msgstr "Música - Todos los artistas del álbum"
|
||||
|
||||
msgctxt "#30352"
|
||||
msgid "Music - Frequently Played"
|
||||
msgstr "Música - Frecuentemente reproducida"
|
||||
|
||||
msgctxt "#30351"
|
||||
msgid "Music - Recently Played"
|
||||
msgstr "Música - Recientemente reproducida"
|
||||
|
||||
msgctxt "#30350"
|
||||
msgid "Music - Recently Added"
|
||||
msgstr "Música - Recientemente añadida"
|
||||
|
||||
msgctxt "#30349"
|
||||
msgid " - Recently Played"
|
||||
msgstr "- Reproducido recientemente"
|
||||
|
||||
msgctxt "#30348"
|
||||
msgid "Add user ratings"
|
||||
msgstr "Añadir valoración de los usuarios"
|
||||
|
||||
msgctxt "#30347"
|
||||
msgid "Getting Existing Images"
|
||||
msgstr "Obteniendo las imágenes existentes"
|
||||
|
||||
msgctxt "#30346"
|
||||
msgid "Deleteing Cached Images"
|
||||
msgstr "Borrando imágenes en caché"
|
||||
|
||||
msgctxt "#30344"
|
||||
msgid "Number of images removed from cache"
|
||||
msgstr "Número de imágenes eliminadas del caché"
|
||||
|
||||
msgctxt "#30343"
|
||||
msgid "Changes Require Kodi Restart"
|
||||
msgstr "Los cambios requieren que Kodi se reinicie"
|
||||
|
||||
msgctxt "#30342"
|
||||
msgid "New content check interval (0 = disabled)"
|
||||
msgstr "Intervalo de comprobación de nuevo contenido (0 = deshabilitado)"
|
||||
|
||||
msgctxt "#30341"
|
||||
msgid "Background image update interval (0 = disabled)"
|
||||
msgstr "Intervalo de actualización de la imagen de fondo (0 = deshabilitado)"
|
||||
|
||||
msgctxt "#30340"
|
||||
msgid "Group movies into collections"
|
||||
msgstr "Agrupar películas en colecciones"
|
||||
|
||||
msgctxt "#30339"
|
||||
msgid "Person"
|
||||
msgstr "Persona"
|
||||
|
||||
msgctxt "#30338"
|
||||
msgid "Album"
|
||||
msgstr "Álbum"
|
||||
|
||||
msgctxt "#30337"
|
||||
msgid "Song"
|
||||
msgstr "Canción"
|
||||
|
||||
msgctxt "#30334"
|
||||
msgid "Use JellyCon context menu"
|
||||
msgstr "Usar el menú contextual de JellyCon"
|
||||
|
||||
msgctxt "#30332"
|
||||
msgid "Stop media playback on screensaver activation"
|
||||
msgstr "Parar la reproducción de medios al activar el salvapantallas"
|
||||
|
||||
msgctxt "#30331"
|
||||
msgid "Movies per page"
|
||||
msgstr "Películas por página"
|
||||
|
||||
msgctxt "#30330"
|
||||
msgid "Show change user dialog"
|
||||
msgstr "Mostrar diálogo de cambio de usuario"
|
||||
|
||||
msgctxt "#30329"
|
||||
msgid "Screensaver"
|
||||
msgstr "Salvapantallas"
|
||||
|
||||
msgctxt "#30328"
|
||||
msgid "Show empty folders (shows, seasons, collections)"
|
||||
msgstr "Mostrar carpetas vacías (series, temporadas, colecciones)"
|
||||
|
||||
msgctxt "#30327"
|
||||
msgid "Go To Season"
|
||||
msgstr "Ir a la temporada"
|
||||
|
||||
msgctxt "#30325"
|
||||
msgid " - Genres"
|
||||
msgstr "- Géneros"
|
||||
|
||||
msgctxt "#30322"
|
||||
msgid "Auto resume"
|
||||
msgstr "Reanudar automáticamente"
|
||||
|
||||
msgctxt "#30320"
|
||||
msgid " - Albums"
|
||||
msgstr "- Álbumes"
|
||||
|
||||
msgctxt "#30318"
|
||||
msgid "Music - Albums"
|
||||
msgstr "Música - Álbumes"
|
||||
|
||||
msgctxt "#30317"
|
||||
msgid "Play All"
|
||||
msgstr "Reproducir todo"
|
||||
|
||||
msgctxt "#30316"
|
||||
msgid "Connection Error"
|
||||
msgstr "Error de conexión"
|
||||
|
||||
msgctxt "#30315"
|
||||
msgid "Suppress notifications for connection errors"
|
||||
msgstr "Suprimir notificaciones por errores de conexión"
|
||||
|
||||
msgctxt "#30314"
|
||||
msgid "Play"
|
||||
msgstr "Reproducir"
|
||||
|
||||
msgctxt "#30313"
|
||||
msgid "Menu"
|
||||
msgstr "Menú"
|
||||
|
||||
msgctxt "#30312"
|
||||
msgid "All - "
|
||||
msgstr "Todo -"
|
||||
|
||||
msgctxt "#30311"
|
||||
msgid "Library - "
|
||||
msgstr "Biblioteca -"
|
||||
|
||||
msgctxt "#30310"
|
||||
msgid "Enable Jellyfin remote control"
|
||||
msgstr "Activar el control remoto de Jellyfin"
|
||||
|
||||
msgctxt "#30309"
|
||||
msgid "Select Media Source"
|
||||
msgstr "Elegir fuente de medios"
|
||||
|
||||
msgctxt "#30308"
|
||||
msgid "Select Trailer"
|
||||
msgstr "Elegir tráiler"
|
||||
|
||||
msgctxt "#30307"
|
||||
msgid "Play Trailer"
|
||||
msgstr "Reproducir tráiler"
|
||||
|
||||
msgctxt "#30306"
|
||||
msgid "Playback starting"
|
||||
msgstr "Empezando la reproducción"
|
||||
|
||||
msgctxt "#30305"
|
||||
msgid "Not Found"
|
||||
msgstr "No encontrado"
|
||||
|
||||
msgctxt "#30304"
|
||||
msgid "Cached Jellyfin images : "
|
||||
msgstr "Imágenes de Jellyfin en caché:"
|
||||
|
||||
msgctxt "#30303"
|
||||
msgid "Missing Jellyfin images : "
|
||||
msgstr "Imágenes de Jellyfin faltantes:"
|
||||
|
||||
msgctxt "#30302"
|
||||
msgid "Existing images : "
|
||||
msgstr "Imágenes existentes:"
|
||||
|
||||
msgctxt "#30301"
|
||||
msgid "Caching Images"
|
||||
msgstr "Guardando imágenes en caché"
|
||||
|
||||
msgctxt "#30300"
|
||||
msgid "Cache all Jellyfin images as local Kodi images?"
|
||||
msgstr ""
|
||||
"¿Guardar en caché todas las imágenes de Jellyfin como imágenes de Kodi "
|
||||
"locales?"
|
||||
|
||||
msgctxt "#30298"
|
||||
msgid "Deleting Kodi Images"
|
||||
msgstr "Borrando las imágenes de Kodi"
|
||||
|
||||
msgctxt "#30297"
|
||||
msgid "Delete unused images?"
|
||||
msgstr "¿Borrar imágenes no usadas?"
|
||||
|
||||
msgctxt "#30296"
|
||||
msgid "Delete"
|
||||
msgstr "Borrar"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30295"
|
||||
msgid "To use this feature you need HTTP control enabled"
|
||||
msgstr "Para usar esta característica necesitas activar el control HTTP"
|
||||
|
||||
msgctxt "#30294"
|
||||
msgid "Notice"
|
||||
msgstr "Aviso"
|
||||
|
||||
msgctxt "#30293"
|
||||
msgid "Cache images"
|
||||
msgstr "Guardar imágenes en caché"
|
||||
|
||||
msgctxt "#30292"
|
||||
msgid "Select Subtitle Stream"
|
||||
msgstr "Seleccionar pista de subtítulos"
|
||||
|
||||
msgctxt "#30291"
|
||||
msgid "Select Audio Stream"
|
||||
msgstr "Seleccionar pista de audio"
|
||||
|
||||
msgctxt "#30290"
|
||||
msgid "All"
|
||||
msgstr "Todo"
|
||||
|
||||
msgctxt "#30289"
|
||||
msgid "TV Shows - Genres"
|
||||
msgstr "Series - Generos"
|
||||
|
||||
msgctxt "#30288"
|
||||
msgid " - Latest"
|
||||
msgstr "- Último"
|
||||
|
||||
msgctxt "#30287"
|
||||
msgid "TV Shows - Latest"
|
||||
msgstr "Series - Último"
|
||||
|
||||
msgctxt "#30286"
|
||||
msgid "Movies - Unwatched"
|
||||
msgstr "Películas - No vistas"
|
||||
|
||||
msgctxt "#30285"
|
||||
msgid " - Unwatched"
|
||||
msgstr "- No visto"
|
||||
|
||||
msgctxt "#30283"
|
||||
msgid "Play Next Episode?"
|
||||
msgstr "¿Reproducir episodio siguiente?"
|
||||
|
||||
msgctxt "#30282"
|
||||
msgid "No Jellyfin servers detected on your local network."
|
||||
msgstr "No se han detectado servidores de Jellyfin en tu red local."
|
||||
|
||||
msgctxt "#30281"
|
||||
msgid "Refresh Cached Images"
|
||||
msgstr "Actualizar imágenes en caché"
|
||||
|
||||
msgctxt "#30280"
|
||||
msgid "Missing Title"
|
||||
msgstr "Título faltante"
|
||||
|
||||
msgctxt "#30279"
|
||||
msgid "TV Shows - Unwatched"
|
||||
msgstr "Series - No vistas"
|
||||
|
||||
msgctxt "#30278"
|
||||
msgid " - Next Up"
|
||||
msgstr "- A continuación"
|
||||
|
||||
msgctxt "#30275"
|
||||
msgid "Force Transcode"
|
||||
msgstr "Forzar transcodificación"
|
||||
|
||||
msgctxt "#30274"
|
||||
msgid "Delete"
|
||||
msgstr "Borrar"
|
||||
|
||||
msgctxt "#30273"
|
||||
msgid "Unset Favourite"
|
||||
msgstr "Quitar favorito"
|
||||
|
||||
msgctxt "#30272"
|
||||
msgid "Set Favourite"
|
||||
msgstr "Establecer como favorito"
|
||||
|
||||
msgctxt "#30271"
|
||||
msgid "Mark Unwatched"
|
||||
msgstr "Marcar como no visto"
|
||||
|
||||
msgctxt "#30270"
|
||||
msgid "Mark Watched"
|
||||
msgstr "Marcar como visto"
|
||||
|
||||
msgctxt "#30269"
|
||||
msgid "Movies - Random"
|
||||
msgstr "Películas - Al azar"
|
||||
|
||||
msgctxt "#30268"
|
||||
msgid " - Recently Added"
|
||||
msgstr "- Añadido recientemente"
|
||||
|
||||
msgctxt "#30267"
|
||||
msgid " - In Progress"
|
||||
msgstr "- En progreso"
|
||||
|
||||
msgctxt "#30266"
|
||||
msgid "Movies - Pages"
|
||||
msgstr "Películas - Páginas"
|
||||
|
||||
msgctxt "#30265"
|
||||
msgid "Episodes - Next Up"
|
||||
msgstr "Episodios - A continuación"
|
||||
|
||||
msgctxt "#30264"
|
||||
msgid "Episodes - In Progress"
|
||||
msgstr "Episodios - En progreso"
|
||||
|
||||
msgctxt "#30263"
|
||||
msgid "Episodes - Recently Added"
|
||||
msgstr "Episodios - Añadidos recientemente"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "Series - Favoritos"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "Series"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "Películas - Favoritos"
|
||||
|
||||
msgctxt "#30258"
|
||||
msgid "Movies - In Progress"
|
||||
msgstr "Películas - En progreso"
|
||||
|
||||
msgctxt "#30257"
|
||||
msgid "Movies - Recently Added"
|
||||
msgstr "Películas - Añadidas recientemente"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "Películas"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "Series - A-Z"
|
||||
|
||||
msgctxt "#30254"
|
||||
msgid "Show add-on settings"
|
||||
msgstr "Mostrar configuración del complemento"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "Películas - A-Z"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "Películas - Géneros"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "Desconocido"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
msgctxt "#30241"
|
||||
msgid "Force transcode mpeg4"
|
||||
msgstr "Forzar transcodificación MPEG4"
|
||||
|
||||
msgctxt "#30240"
|
||||
msgid "Force transcode msmpeg4v3 (divx)"
|
||||
msgstr "Forzar transcodificación MSMPEG4v3 (DivX)"
|
||||
|
||||
msgctxt "#30239"
|
||||
msgid "Force transcode mpeg2"
|
||||
msgstr "Forzar transcodificación MPEG2"
|
||||
|
||||
msgctxt "#30238"
|
||||
msgid "Playback stream options"
|
||||
msgstr "Opciones de reproducción"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "Empezar desde el principio"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "Forzar transcodificación H.265 (HEVC)"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "Episodios"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "Películas"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "Series"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "Interacción"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "Tamaño de página y filtrado"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "Disposición del elemento"
|
||||
|
||||
msgctxt "#30220"
|
||||
msgid "Prompt to delete movie after %"
|
||||
msgstr "Preguntar para borrar la película después de %"
|
||||
|
||||
msgctxt "#30219"
|
||||
msgid " - Prompt before play"
|
||||
msgstr "- Preguntar antes de reproducir"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "Reproducir siguiente episodio después de %"
|
||||
|
||||
msgctxt "#30217"
|
||||
msgid "Prompt to delete episode after %"
|
||||
msgstr "Preguntar para borrar el episodio después de %"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "Detalles del elemento"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "Eventos"
|
||||
|
||||
msgctxt "#30213"
|
||||
msgid "Video force 8 bit"
|
||||
msgstr "Forzar 8 bits en el vídeo"
|
||||
|
||||
msgctxt "#30212"
|
||||
msgid "Video max width"
|
||||
msgstr "Ancho máximo del vídeo"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "Opciones de transcodificación"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30209"
|
||||
msgid "File direct path"
|
||||
msgstr "Ruta directa al archivo"
|
||||
|
||||
msgctxt "#30208"
|
||||
msgid "Max stream bitrate (Kbits)"
|
||||
msgstr "Máxima tasa de bits (Kbps)"
|
||||
|
||||
msgctxt "#30207"
|
||||
msgid "Playback"
|
||||
msgstr "Reproducción"
|
||||
|
||||
msgctxt "#30206"
|
||||
msgid "Playback type"
|
||||
msgstr "Tipo de reproducción"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "No se ha podido conectar al servidor"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "Error de URL"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "Incluir gente"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "Incluir trama"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "Usuario seleccionado"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "Dirección:"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "Dirección del servidor seleccionado"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "Seleccionar servidor"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "Procesando el elemento:"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "Hecho"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "Mostrar el progreso de carga"
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Add resume percent to names"
|
||||
msgstr "Añadir el porcentaje de reanudación a los nombres"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "Recuperando los datos"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "Cargando contenido"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "Servicios"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "Interfaz"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr "Aviso: Esta acción borrará los archivos de los medios del servidor."
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "¿Confirmar borrado?"
|
||||
|
||||
msgctxt "#30063"
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "Esperando al servidor para borrar"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "Borrando"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "Nombre de usuario no encontrado"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "Nombre de usuario/Contraseña incorrectos"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Activar registro de depuración"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Contraseña:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Nombre de usuario:"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "Esconder los detalles de los episodios no vistos"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Avanzado"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "Mostrar todos los episodios"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Formato de nombre de episodio filtrado"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Número de elementos a mostrar en las listas filtradas"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Mostrar clientes conectados"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Nombre a mostrar del dispositivo"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Cambiar de usuario]"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Detectar servidor local]"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "Número de perfiles de rendimiento a capturar"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Contraseña de Samba"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Nombre de usuario de Samba"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Nombre de usuario"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "Verificar certificado HTTPS"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Puerto"
|
||||
2
resources/language/resource.language.fr/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.hi/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"
|
||||
1089
resources/language/resource.language.hu/strings.po
Normal file
428
resources/language/resource.language.it/strings.po
Normal file
@@ -0,0 +1,428 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2021-12-02 18:05+0000\n"
|
||||
"Last-Translator: Alfonso Scarpino <alfonso.scarpino@gmail.com>\n"
|
||||
"Language-Team: Italian <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/it/>\n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.5.2\n"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "Mostra avanzamento del caricamento"
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Add resume percent to names"
|
||||
msgstr "Aggiungi percentuale guardata ai nomi"
|
||||
|
||||
msgctxt "#30116"
|
||||
msgid "Add unwatched counts to names"
|
||||
msgstr "Aggiungi il contatore non guardati ai nomi"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "Recupero dati in corso"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "Caricamento contenuto in corso"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "Servizi"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "Interfaccia"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr "Attenzione: questa operazione eliminerà i file multimediali dal server."
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "Confermi l'eliminazione?"
|
||||
|
||||
msgctxt "#30063"
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "In attesa di eliminazione sul server"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "Eliminazione in corso"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "Nome utente sconosciuto"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "Nome utente/Password errati"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Attiva log a debug"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "Azione di selezione elemento widget"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Password:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Nome utente:"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "Nascondi dettagli episodi non guardati"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Avanzate"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "Mostra elemento tutti gli episodi"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "Appiattisci stagione unica"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Formato nome episodio filtrato"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Numero di elementi da mostrare nelle liste filtrate"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Mostra client connessi"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Nome dispositivo visualizzato"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "Dati di temporizzazione dei log"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Cambia utente]"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Rileva server locale]"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "Numero di profili di performance da acquisire"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Password Samba"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Nome utente Samba"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Nome utente"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "Verifica certificato HTTPS"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Porta"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Host"
|
||||
|
||||
msgctxt "#30207"
|
||||
msgid "Playback"
|
||||
msgstr "Riproduzione"
|
||||
|
||||
msgctxt "#30206"
|
||||
msgid "Playback type"
|
||||
msgstr "Tipo riproduzione"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "Impossibile connettersi al server"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "Errore URL"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "Includi persone"
|
||||
|
||||
msgctxt "#30182"
|
||||
msgid "Include media stream info"
|
||||
msgstr "Includi info flusso multimediale"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "Includi trama"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "Seleziona Utente"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "Indirizzo:"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "Indirizzo server selezionato"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "Seleziona Server"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "Aggiungi (cc) se sono disponibili i sottotitoli"
|
||||
|
||||
msgctxt "#30139"
|
||||
msgid "No Media Type Set"
|
||||
msgstr "Nessun formato media impostato"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "Errore"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "Elaborazione Elemento:"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "Finito"
|
||||
|
||||
msgctxt "#30275"
|
||||
msgid "Force Transcode"
|
||||
msgstr "Forza transcodifica"
|
||||
|
||||
msgctxt "#30274"
|
||||
msgid "Delete"
|
||||
msgstr "Elimina"
|
||||
|
||||
msgctxt "#30273"
|
||||
msgid "Unset Favourite"
|
||||
msgstr "Rimuovi dai preferiti"
|
||||
|
||||
msgctxt "#30272"
|
||||
msgid "Set Favourite"
|
||||
msgstr "Aggiungi ai preferiti"
|
||||
|
||||
msgctxt "#30271"
|
||||
msgid "Mark Unwatched"
|
||||
msgstr "Segna come non guardato"
|
||||
|
||||
msgctxt "#30270"
|
||||
msgid "Mark Watched"
|
||||
msgstr "Segna come guardato"
|
||||
|
||||
msgctxt "#30269"
|
||||
msgid "Movies - Random"
|
||||
msgstr "Film - Casuale"
|
||||
|
||||
msgctxt "#30268"
|
||||
msgid " - Recently Added"
|
||||
msgstr "- Aggiunti di recente"
|
||||
|
||||
msgctxt "#30267"
|
||||
msgid " - In Progress"
|
||||
msgstr "- In corso"
|
||||
|
||||
msgctxt "#30266"
|
||||
msgid "Movies - Pages"
|
||||
msgstr "Film - Pagine"
|
||||
|
||||
msgctxt "#30265"
|
||||
msgid "Episodes - Next Up"
|
||||
msgstr "Episodi - Prossimo"
|
||||
|
||||
msgctxt "#30264"
|
||||
msgid "Episodes - In Progress"
|
||||
msgstr "Episodi - In corso"
|
||||
|
||||
msgctxt "#30263"
|
||||
msgid "Episodes - Recently Added"
|
||||
msgstr "Episodi - Aggiunti di recente"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "Serie TV - Preferiti"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "Serie TV"
|
||||
|
||||
msgctxt "#30260"
|
||||
msgid "BoxSets"
|
||||
msgstr "Collezioni"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "Film - Preferiti"
|
||||
|
||||
msgctxt "#30258"
|
||||
msgid "Movies - In Progress"
|
||||
msgstr "Film - In corso"
|
||||
|
||||
msgctxt "#30257"
|
||||
msgid "Movies - Recently Added"
|
||||
msgstr "Film - Aggiunti di recente"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "Film"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "Serie TV - A-Z"
|
||||
|
||||
msgctxt "#30254"
|
||||
msgid "Show add-on settings"
|
||||
msgstr "Mostra impostazioni add-on"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "Film - A-Z"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "Film - Generi"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "Sconosciuto"
|
||||
|
||||
msgctxt "#30247"
|
||||
msgid "Custom Widget Content"
|
||||
msgstr "Contenuto personalizzato widget"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "Cerca"
|
||||
|
||||
msgctxt "#30241"
|
||||
msgid "Force transcode mpeg4"
|
||||
msgstr "Forza transcodifica mpeg4"
|
||||
|
||||
msgctxt "#30240"
|
||||
msgid "Force transcode msmpeg4v3 (divx)"
|
||||
msgstr "Forza transcodifica msmpeg4v3 (divx)"
|
||||
|
||||
msgctxt "#30239"
|
||||
msgid "Force transcode mpeg2"
|
||||
msgstr "Forza transcodifica mpeg2"
|
||||
|
||||
msgctxt "#30238"
|
||||
msgid "Playback stream options"
|
||||
msgstr "Opzioni di riproduzione flusso"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "Ricomincia dall'inizio"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "Forza transcodifica h265 (hevc)"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "Episodi"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "Film"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "Serie TV"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "Interazione"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "Dimensione pagina e filtri"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "Disposizione elemento"
|
||||
|
||||
msgctxt "#30220"
|
||||
msgid "Prompt to delete movie after %"
|
||||
msgstr "Chiedi se eliminare il film dopo %"
|
||||
|
||||
msgctxt "#30219"
|
||||
msgid " - Prompt before play"
|
||||
msgstr "- Chiedi prima di riprodurre"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "Riproduci il prossimo episodio dopo %"
|
||||
|
||||
msgctxt "#30217"
|
||||
msgid "Prompt to delete episode after %"
|
||||
msgstr "Chiedi se eliminare l'episodio dopo %"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "Dettagli elemento"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "Eventi"
|
||||
|
||||
msgctxt "#30212"
|
||||
msgid "Video max width"
|
||||
msgstr "Larghezza massima video"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "Opzioni di transcodifica"
|
||||
|
||||
msgctxt "#30210"
|
||||
msgid "HTTP direct stream"
|
||||
msgstr "Flusso diretto HTTP"
|
||||
|
||||
msgctxt "#30209"
|
||||
msgid "File direct path"
|
||||
msgstr "Percorso diretto del file"
|
||||
|
||||
msgctxt "#30208"
|
||||
msgid "Max stream bitrate (Kbits)"
|
||||
msgstr "Bitrate massimo del flusso (Kbits)"
|
||||
1091
resources/language/resource.language.kk/strings.po
Normal file
2
resources/language/resource.language.nl/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"
|
||||
661
resources/language/resource.language.pl/strings.po
Normal file
@@ -0,0 +1,661 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2021-11-23 03:05+0000\n"
|
||||
"Last-Translator: Marcin Woliński <cierdek@gmail.com>\n"
|
||||
"Language-Team: Polish <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/pl/>\n"
|
||||
"Language: pl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.5.2\n"
|
||||
|
||||
msgctxt "#30313"
|
||||
msgid "Menu"
|
||||
msgstr "Menu"
|
||||
|
||||
msgctxt "#30312"
|
||||
msgid "All - "
|
||||
msgstr "Wszystko -"
|
||||
|
||||
msgctxt "#30311"
|
||||
msgid "Library - "
|
||||
msgstr "Biblioteka -"
|
||||
|
||||
msgctxt "#30310"
|
||||
msgid "Enable Jellyfin remote control"
|
||||
msgstr "Włącz zdalne sterowanie Jellyfin"
|
||||
|
||||
msgctxt "#30309"
|
||||
msgid "Select Media Source"
|
||||
msgstr "Wybierz źródło multimediów"
|
||||
|
||||
msgctxt "#30308"
|
||||
msgid "Select Trailer"
|
||||
msgstr "Wybierz zwiastun"
|
||||
|
||||
msgctxt "#30307"
|
||||
msgid "Play Trailer"
|
||||
msgstr "Odtwórz zwiastun"
|
||||
|
||||
msgctxt "#30306"
|
||||
msgid "Playback starting"
|
||||
msgstr "Rozpoczęcie odtwarzania"
|
||||
|
||||
msgctxt "#30305"
|
||||
msgid "Not Found"
|
||||
msgstr "Nie znaleziono"
|
||||
|
||||
msgctxt "#30304"
|
||||
msgid "Cached Jellyfin images : "
|
||||
msgstr "Buforowane obrazy Jellyfin:"
|
||||
|
||||
msgctxt "#30303"
|
||||
msgid "Missing Jellyfin images : "
|
||||
msgstr "Brakujące obrazy Jellyfin:"
|
||||
|
||||
msgctxt "#30302"
|
||||
msgid "Existing images : "
|
||||
msgstr "Istniejące obrazy:"
|
||||
|
||||
msgctxt "#30301"
|
||||
msgid "Caching Images"
|
||||
msgstr "Buforowanie obrazów"
|
||||
|
||||
msgctxt "#30300"
|
||||
msgid "Cache all Jellyfin images as local Kodi images?"
|
||||
msgstr "Buforować wszystkie obrazy Jellyfin jako lokalne obrazy Kodi?"
|
||||
|
||||
msgctxt "#30299"
|
||||
msgid "Cache Images"
|
||||
msgstr "Obrazy w pamięci podręcznej"
|
||||
|
||||
msgctxt "#30298"
|
||||
msgid "Deleting Kodi Images"
|
||||
msgstr "Usuwanie obrazów Kodi"
|
||||
|
||||
msgctxt "#30297"
|
||||
msgid "Delete unused images?"
|
||||
msgstr "Usunąć nieużywane obrazy?"
|
||||
|
||||
msgctxt "#30296"
|
||||
msgid "Delete"
|
||||
msgstr "Usuń"
|
||||
|
||||
msgctxt "#30295"
|
||||
msgid "To use this feature you need HTTP control enabled"
|
||||
msgstr "Aby korzystać z tej funkcji, musisz mieć włączoną kontrolę HTTP"
|
||||
|
||||
msgctxt "#30294"
|
||||
msgid "Notice"
|
||||
msgstr "Uwagi"
|
||||
|
||||
msgctxt "#30293"
|
||||
msgid "Cache images"
|
||||
msgstr "Pamięć podręczna obrazów"
|
||||
|
||||
msgctxt "#30292"
|
||||
msgid "Select Subtitle Stream"
|
||||
msgstr "Wybierz ścieżkę napisów"
|
||||
|
||||
msgctxt "#30291"
|
||||
msgid "Select Audio Stream"
|
||||
msgstr "Wybierz ścieżkę audio"
|
||||
|
||||
msgctxt "#30290"
|
||||
msgid "All"
|
||||
msgstr "Wszystko"
|
||||
|
||||
msgctxt "#30289"
|
||||
msgid "TV Shows - Genres"
|
||||
msgstr "Seriale - Gatunki"
|
||||
|
||||
msgctxt "#30288"
|
||||
msgid " - Latest"
|
||||
msgstr "- Ostatnie"
|
||||
|
||||
msgctxt "#30287"
|
||||
msgid "TV Shows - Latest"
|
||||
msgstr "Seriale — ostatnie"
|
||||
|
||||
msgctxt "#30286"
|
||||
msgid "Movies - Unwatched"
|
||||
msgstr "Filmy – nieobejrzane"
|
||||
|
||||
msgctxt "#30285"
|
||||
msgid " - Unwatched"
|
||||
msgstr "- Nieobejrzane"
|
||||
|
||||
msgctxt "#30283"
|
||||
msgid "Play Next Episode?"
|
||||
msgstr "Odtworzyć kolejny odcinek?"
|
||||
|
||||
msgctxt "#30282"
|
||||
msgid "No Jellyfin servers detected on your local network."
|
||||
msgstr "W twojej sieci lokalnej nie wykryto serwerów Jellyfin."
|
||||
|
||||
msgctxt "#30281"
|
||||
msgid "Refresh Cached Images"
|
||||
msgstr "Odśwież obrazy z pamięci podręcznej"
|
||||
|
||||
msgctxt "#30280"
|
||||
msgid "Missing Title"
|
||||
msgstr "Brak tytułu"
|
||||
|
||||
msgctxt "#30279"
|
||||
msgid "TV Shows - Unwatched"
|
||||
msgstr "Seriale - Nieoglądane"
|
||||
|
||||
msgctxt "#30278"
|
||||
msgid " - Next Up"
|
||||
msgstr "- Następna w górę"
|
||||
|
||||
msgctxt "#30277"
|
||||
msgid "JellyCon needs to prompt for resume on partily played items, Kodi can also prompt, this can cause a double prompt. Do you want to remove the double prompt?"
|
||||
msgstr ""
|
||||
"JellyCon musi monitować o wznowienie w przypadku częściowo odtwarzanych "
|
||||
"elementów, Kodi może również monitować, co może spowodować podwójny monit. "
|
||||
"Czy chcesz usunąć podwójny monit?"
|
||||
|
||||
msgctxt "#30276"
|
||||
msgid "Extra Resume Prompt Detected"
|
||||
msgstr "Wykryto dodatkowy monit o wznowienie"
|
||||
|
||||
msgctxt "#30275"
|
||||
msgid "Force Transcode"
|
||||
msgstr "Wymuś transkodowanie"
|
||||
|
||||
msgctxt "#30274"
|
||||
msgid "Delete"
|
||||
msgstr "Usuń"
|
||||
|
||||
msgctxt "#30273"
|
||||
msgid "Unset Favourite"
|
||||
msgstr "Usuń z ulubionych"
|
||||
|
||||
msgctxt "#30272"
|
||||
msgid "Set Favourite"
|
||||
msgstr "Ustaw ulubione"
|
||||
|
||||
msgctxt "#30271"
|
||||
msgid "Mark Unwatched"
|
||||
msgstr "Oznacz nieoglądane"
|
||||
|
||||
msgctxt "#30270"
|
||||
msgid "Mark Watched"
|
||||
msgstr "Zaznacz obserwowane"
|
||||
|
||||
msgctxt "#30269"
|
||||
msgid "Movies - Random"
|
||||
msgstr "Filmy — losowe"
|
||||
|
||||
msgctxt "#30268"
|
||||
msgid " - Recently Added"
|
||||
msgstr "- Niedawno dodany"
|
||||
|
||||
msgctxt "#30267"
|
||||
msgid " - In Progress"
|
||||
msgstr "- W trakcie"
|
||||
|
||||
msgctxt "#30266"
|
||||
msgid "Movies - Pages"
|
||||
msgstr "Filmy – Strony"
|
||||
|
||||
msgctxt "#30265"
|
||||
msgid "Episodes - Next Up"
|
||||
msgstr "Odcinki — Następny"
|
||||
|
||||
msgctxt "#30264"
|
||||
msgid "Episodes - In Progress"
|
||||
msgstr "Odcinki — w toku"
|
||||
|
||||
msgctxt "#30263"
|
||||
msgid "Episodes - Recently Added"
|
||||
msgstr "Odcinki — ostatnio dodane"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "Seriale – Ulubione"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "Seriale"
|
||||
|
||||
msgctxt "#30260"
|
||||
msgid "BoxSets"
|
||||
msgstr "Zestawy pudełek"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "Filmy - Ulubione"
|
||||
|
||||
msgctxt "#30258"
|
||||
msgid "Movies - In Progress"
|
||||
msgstr "Filmy – w toku"
|
||||
|
||||
msgctxt "#30257"
|
||||
msgid "Movies - Recently Added"
|
||||
msgstr "Filmy — ostatnio dodane"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "Filmy"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "Seriale - A-Z"
|
||||
|
||||
msgctxt "#30254"
|
||||
msgid "Show add-on settings"
|
||||
msgstr "Pokaż ustawienia dodatków"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "Filmy - A-Z"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "Filmy – Gatunki"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "Nieznane"
|
||||
|
||||
msgctxt "#30247"
|
||||
msgid "Custom Widget Content"
|
||||
msgstr "Niestandardowa treść widżetu"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "Szukaj"
|
||||
|
||||
msgctxt "#30241"
|
||||
msgid "Force transcode mpeg4"
|
||||
msgstr "Wymuś transkodowanie mpeg4"
|
||||
|
||||
msgctxt "#30240"
|
||||
msgid "Force transcode msmpeg4v3 (divx)"
|
||||
msgstr "Wymuś transkodowanie msmpeg4v3 (divx)"
|
||||
|
||||
msgctxt "#30239"
|
||||
msgid "Force transcode mpeg2"
|
||||
msgstr "Wymuś transkodowanie mpeg2"
|
||||
|
||||
msgctxt "#30238"
|
||||
msgid "Playback stream options"
|
||||
msgstr "Opcje strumienia odtwarzania"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "Zacznij od początku"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "Wymuś transkodowanie h265 (hevc)"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "Odcinki"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "Filmy"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "Seriale"
|
||||
|
||||
msgctxt "#30213"
|
||||
msgid "Video force 8 bit"
|
||||
msgstr "Wymuś video 8 bit"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "Interakcja"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "Rozmiar strony i filtrowanie"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "Układ przedmiotu"
|
||||
|
||||
msgctxt "#30220"
|
||||
msgid "Prompt to delete movie after %"
|
||||
msgstr "Pytaj o usunięcie filmu po %"
|
||||
|
||||
msgctxt "#30219"
|
||||
msgid " - Prompt before play"
|
||||
msgstr "- Pytaj przed odtworzeniem"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "Odtwórz następny odcinek po %"
|
||||
|
||||
msgctxt "#30217"
|
||||
msgid "Prompt to delete episode after %"
|
||||
msgstr "Monituj o usunięcie odcinka po %"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "Szczegóły produktu"
|
||||
|
||||
msgctxt "#30215"
|
||||
msgid "On playback stop (100% = disabled)"
|
||||
msgstr "Po odtworzeniu zatrzymaj (100% = wyłączone)"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "Zdarzenia"
|
||||
|
||||
msgctxt "#30212"
|
||||
msgid "Video max width"
|
||||
msgstr "Maksymalna szerokość wideo"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "Opcje transkodowania"
|
||||
|
||||
msgctxt "#30210"
|
||||
msgid "HTTP direct stream"
|
||||
msgstr "Bezpośredni strumień HTTP"
|
||||
|
||||
msgctxt "#30209"
|
||||
msgid "File direct path"
|
||||
msgstr "Bezpośrednia ścieżka pliku"
|
||||
|
||||
msgctxt "#30208"
|
||||
msgid "Max stream bitrate (Kbits)"
|
||||
msgstr "Maksymalna transmisja strumienia (Kb/s)"
|
||||
|
||||
msgctxt "#30207"
|
||||
msgid "Playback"
|
||||
msgstr "Odtwarzanie"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Host"
|
||||
|
||||
msgctxt "#30206"
|
||||
msgid "Playback type"
|
||||
msgstr "Typ odtwarzania"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "Niemożna połączyć z serwerem"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "Błąd adresu URL"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "Uwzględnij osoby"
|
||||
|
||||
msgctxt "#30182"
|
||||
msgid "Include media stream info"
|
||||
msgstr "Dołącz informacje o strumieniu multimediów"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "Uwzględnij fabułę"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "Wybierz użytkownika"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "Adres:"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "Wybierz adres serwera"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "Wybierz serwer"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "Dodaj (cc), jeśli napisy są dostępne"
|
||||
|
||||
msgctxt "#30139"
|
||||
msgid "No Media Type Set"
|
||||
msgstr "Brak zestawu typu nośnika"
|
||||
|
||||
msgctxt "#30321"
|
||||
msgid " - Album Artists"
|
||||
msgstr "- Artyści albumów"
|
||||
|
||||
msgctxt "#30319"
|
||||
msgid "Music - All Album Artists"
|
||||
msgstr "Muzyka — wszyscy wykonawcy albumów"
|
||||
|
||||
msgctxt "#30332"
|
||||
msgid "Stop media playback on screensaver activation"
|
||||
msgstr "Zatrzymaj odtwarzanie multimediów po aktywacji wygaszacza ekranu"
|
||||
|
||||
msgctxt "#30331"
|
||||
msgid "Movies per page"
|
||||
msgstr "Filmów na stronę"
|
||||
|
||||
msgctxt "#30330"
|
||||
msgid "Show change user dialog"
|
||||
msgstr "Pokaż okno dialogowe zmiany użytkownika"
|
||||
|
||||
msgctxt "#30329"
|
||||
msgid "Screensaver"
|
||||
msgstr "Wygaszacz ekranu"
|
||||
|
||||
msgctxt "#30328"
|
||||
msgid "Show empty folders (shows, seasons, collections)"
|
||||
msgstr "Pokaż puste foldery (seriale, sezony, kolekcje)"
|
||||
|
||||
msgctxt "#30327"
|
||||
msgid "Go To Season"
|
||||
msgstr "Przejdź do sezonu"
|
||||
|
||||
msgctxt "#30325"
|
||||
msgid " - Genres"
|
||||
msgstr "- Gatunki"
|
||||
|
||||
msgctxt "#30322"
|
||||
msgid "Auto resume"
|
||||
msgstr "Automatyczne wznawianie"
|
||||
|
||||
msgctxt "#30320"
|
||||
msgid " - Albums"
|
||||
msgstr "- Albumy"
|
||||
|
||||
msgctxt "#30318"
|
||||
msgid "Music - Albums"
|
||||
msgstr "Muzyka - Albumy"
|
||||
|
||||
msgctxt "#30317"
|
||||
msgid "Play All"
|
||||
msgstr "Włącz wszystko"
|
||||
|
||||
msgctxt "#30316"
|
||||
msgid "Connection Error"
|
||||
msgstr "Błąd połączenia"
|
||||
|
||||
msgctxt "#30315"
|
||||
msgid "Suppress notifications for connection errors"
|
||||
msgstr "Pomiń powiadomienia o błędach połączenia"
|
||||
|
||||
msgctxt "#30314"
|
||||
msgid "Play"
|
||||
msgstr "Włącz"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "Błąd"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "Przetwarzanie przedmiotu:"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "Gotowe"
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "On resume"
|
||||
msgstr "Po wznowieniu"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "Pokaż postęp ładowania"
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Add resume percent to names"
|
||||
msgstr "Dodaj wznowiony procent do nazwisk"
|
||||
|
||||
msgctxt "#30116"
|
||||
msgid "Add unwatched counts to names"
|
||||
msgstr "Dodaj nieobejrzane liczby do nazwisk"
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "Cofnij się o kilka sekund"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "Pobieranie danych"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "Ładowanie treści"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "Usługi"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "Interfejs"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr ""
|
||||
"Ostrzeżenie: Ta akcja spowoduje usunięcie plików multimedialnych z serwera."
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "Potwierdź usunięcie?"
|
||||
|
||||
msgctxt "#30063"
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "Czekam na usunięcie serwera"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "Kasowanie"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "Nazwa użytkownika nie znaleziona"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "Nieprawidłowa nazwa użytkownika / hasło"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Włącz rejestrowanie debugowania"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "Akcja wyboru elementu widżetu"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Hasło:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Nazwa użytkownika:"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "Ukryj szczegóły nieobejrzanych odcinków"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Zaawansowane"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "Pokaż wszystkie odcinki"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "Spłaszcz jeden sezon"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Filtrowany format nazwy odcinka"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Liczba pozycji do pokazania na przefiltrowanych listach"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Pokaż połączonych klientów"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Wyświetlana nazwa urządzenia"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "Rejestruj dane czasowe"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Zmień użytkownika]"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Wykryj serwer lokalny]"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "Liczba profili wydajności do uchwycenia"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Hasło użytkownika Samby"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Nazwa użytkownika Samby"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Hasło"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Nazwa użytkownika"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "Zweryfikuj certyfikat HTTPS"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
1088
resources/language/resource.language.ru/strings.po
Normal file
2
resources/language/resource.language.tr/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.uk/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"
|
||||
1084
resources/language/resource.language.zh_Hans/strings.po
Normal file
2
resources/language/resource.language.zh_Hant/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"
|
||||
@@ -1,4 +1,5 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import time
|
||||
import threading
|
||||
@@ -6,9 +7,9 @@ import threading
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ActionAutoClose(threading.Thread):
|
||||
@@ -25,9 +26,9 @@ class ActionAutoClose(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
log.debug("ActionAutoClose Running")
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
time_since_last = time.time() - self.last_interaction
|
||||
log.debug("ActionAutoClose time_since_last : {0}", time_since_last)
|
||||
log.debug("ActionAutoClose time_since_last : {0}".format(time_since_last))
|
||||
|
||||
if time_since_last > 20:
|
||||
log.debug("ActionAutoClose Closing Parent")
|
||||
@@ -40,7 +41,7 @@ class ActionAutoClose(threading.Thread):
|
||||
|
||||
def set_last(self):
|
||||
self.last_interaction = time.time()
|
||||
log.debug("ActionAutoClose set_last : {0}", self.last_interaction)
|
||||
log.debug("ActionAutoClose set_last : {0}".format(self.last_interaction))
|
||||
|
||||
def stop(self):
|
||||
log.debug("ActionAutoClose stop_thread called")
|
||||
@@ -69,9 +70,6 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
self.listControl.addItems(self.action_items)
|
||||
self.setFocus(self.listControl)
|
||||
|
||||
# bg_image = self.getControl(3010)
|
||||
# bg_image.setHeight(50 * len(self.action_items) + 20)
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
@@ -79,7 +77,7 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("ActionMenu: onMessage: {0}", message)
|
||||
log.debug("ActionMenu: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
@@ -91,12 +89,12 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
self.close()
|
||||
else:
|
||||
self.auto_close_thread.set_last()
|
||||
log.debug("ActionMenu: onAction: {0}", action.getId())
|
||||
log.debug("ActionMenu: onAction: {0}".format(action.getId()))
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3000:
|
||||
self.selected_action = self.listControl.getSelectedItem()
|
||||
log.debug("ActionMenu: Selected Item: {0}", self.selected_action)
|
||||
log.debug("ActionMenu: Selected Item: {0}".format(self.selected_action))
|
||||
self.auto_close_thread.stop()
|
||||
self.close()
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
@@ -35,12 +37,10 @@ class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("ActionMenu: onMessage: {0}", 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)
|
||||
|
||||
@@ -54,5 +54,4 @@ class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3000:
|
||||
log.debug("ActionMenu: Selected Item: {0}", control_id)
|
||||
#self.close()
|
||||
log.debug("ActionMenu: Selected Item: {0}".format(control_id))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# coding=utf-8
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
import httplib
|
||||
from six.moves.urllib.parse import unquote
|
||||
import requests
|
||||
import base64
|
||||
import sys
|
||||
import threading
|
||||
@@ -14,15 +15,15 @@ import xbmc
|
||||
import xbmcaddon
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .jsonrpc import JsonRpc
|
||||
from .loghandler import LazyLogger
|
||||
from .jsonrpc import JsonRpc, get_value
|
||||
from .translation import string_load
|
||||
from .datamanager import DataManager
|
||||
from .utils import get_art, double_urlencode
|
||||
from .utils import get_art
|
||||
from .kodi_utils import HomeWindow
|
||||
|
||||
downloadUtils = DownloadUtils()
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class CacheArtwork(threading.Thread):
|
||||
@@ -62,7 +63,7 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
monitor.waitForAbort(5)
|
||||
|
||||
log.debug("CacheArtwork background thread exited : stop_all_activity : {0}", self.stop_all_activity)
|
||||
log.debug("CacheArtwork background thread exited : stop_all_activity : {0}".format(self.stop_all_activity))
|
||||
|
||||
@staticmethod
|
||||
def delete_cached_images(item_id):
|
||||
@@ -74,7 +75,7 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
item_image_url_part = "Items/%s/Images/" % item_id
|
||||
item_image_url_part = item_image_url_part.replace("/", "%2f")
|
||||
log.debug("texture ids: {0}", item_image_url_part)
|
||||
log.debug("texture ids: {0}".format(item_image_url_part))
|
||||
|
||||
# is the web server enabled
|
||||
web_query = {"setting": "services.webserver"}
|
||||
@@ -87,7 +88,7 @@ class CacheArtwork(threading.Thread):
|
||||
params = {"properties": ["url"]}
|
||||
json_result = JsonRpc('Textures.GetTextures').execute(params)
|
||||
textures = json_result.get("result", {}).get("textures", [])
|
||||
log.debug("texture ids: {0}", textures)
|
||||
log.debug("texture ids: {0}".format(textures))
|
||||
|
||||
progress.update(70, string_load(30346))
|
||||
|
||||
@@ -97,7 +98,7 @@ class CacheArtwork(threading.Thread):
|
||||
texture_url = texture["url"]
|
||||
if item_image_url_part in texture_url:
|
||||
delete_count += 1
|
||||
log.debug("removing texture id: {0}", texture_id)
|
||||
log.debug("removing texture id: {0}".format(texture_id))
|
||||
params = {"textureid": int(texture_id)}
|
||||
JsonRpc('Textures.RemoveTexture').execute(params)
|
||||
|
||||
@@ -106,7 +107,7 @@ class CacheArtwork(threading.Thread):
|
||||
progress.update(100, string_load(30125))
|
||||
progress.close()
|
||||
|
||||
xbmcgui.Dialog().ok(string_load(30281), string_load(30344) % delete_count)
|
||||
xbmcgui.Dialog().ok(string_load(30281), '{}: {}'.format(string_load(30344), delete_count))
|
||||
|
||||
def cache_artwork_interactive(self):
|
||||
log.debug("cache_artwork_interactive")
|
||||
@@ -118,7 +119,7 @@ class CacheArtwork(threading.Thread):
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
|
||||
xbmc_webserver_enabled = result['result']['value']
|
||||
if not xbmc_webserver_enabled:
|
||||
xbmcgui.Dialog().ok(string_load(30294), string_load(30295), string_load(30355))
|
||||
xbmcgui.Dialog().ok(string_load(30294), '{} - {}'.format(string_load(30295), string_load(30355)))
|
||||
xbmc.executebuiltin('ActivateWindow(servicesettings)')
|
||||
return
|
||||
|
||||
@@ -141,23 +142,22 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
jellyfin_texture_urls = self.get_jellyfin_artwork(delete_pdialog)
|
||||
|
||||
log.debug("kodi textures: {0}", textures)
|
||||
log.debug("jellyfin texture urls: {0}", jellyfin_texture_urls)
|
||||
log.debug("kodi textures: {0}".format(textures))
|
||||
log.debug("jellyfin texture urls: {0}".format(jellyfin_texture_urls))
|
||||
|
||||
if jellyfin_texture_urls is not None:
|
||||
|
||||
unused_texture_ids = set()
|
||||
for texture in textures:
|
||||
url = texture.get("url")
|
||||
url = urllib.unquote(url)
|
||||
url = unquote(url)
|
||||
url = url.replace("image://", "")
|
||||
url = url[0:-1]
|
||||
if url.find("/") > -1 and url not in jellyfin_texture_urls or url.find("localhost:24276") > -1:
|
||||
# log.debug("adding unused texture url: {0}", url)
|
||||
unused_texture_ids.add(texture["textureid"])
|
||||
|
||||
total = len(unused_texture_ids)
|
||||
log.debug("unused texture ids: {0}", unused_texture_ids)
|
||||
log.debug("unused texture ids: {0}".format(unused_texture_ids))
|
||||
|
||||
for texture_id in unused_texture_ids:
|
||||
params = {"textureid": int(texture_id)}
|
||||
@@ -206,11 +206,11 @@ class CacheArtwork(threading.Thread):
|
||||
try:
|
||||
result_text = self.cache_artwork(dp)
|
||||
except Exception as err:
|
||||
log.error("Cache Images Failed : {0}", err)
|
||||
log.error("Cache Images Failed : {0}".format(err))
|
||||
dp.close()
|
||||
del dp
|
||||
if result_text is not None:
|
||||
log.debug("Cache Images reuslt : {0}", " - ".join(result_text))
|
||||
log.debug("Cache Images reuslt : {0}".format(" - ".join(result_text)))
|
||||
|
||||
def get_jellyfin_artwork(self, progress):
|
||||
log.debug("get_jellyfin_artwork")
|
||||
@@ -233,7 +233,7 @@ class CacheArtwork(threading.Thread):
|
||||
results = results.get("Items")
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
log.debug("Jellyfin Item Count Count: {0}", len(results))
|
||||
log.debug("Jellyfin Item Count Count: {0}".format(len(results)))
|
||||
|
||||
if self.stop_all_activity:
|
||||
return None
|
||||
@@ -242,7 +242,6 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
texture_urls = set()
|
||||
|
||||
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
|
||||
for item in results:
|
||||
art = get_art(item, server)
|
||||
for art_type in art:
|
||||
@@ -254,36 +253,27 @@ class CacheArtwork(threading.Thread):
|
||||
log.debug("cache_artwork")
|
||||
|
||||
# is the web server enabled
|
||||
web_query = {"setting": "services.webserver"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
|
||||
xbmc_webserver_enabled = result['result']['value']
|
||||
if not xbmc_webserver_enabled:
|
||||
if not get_value("services.webserver"):
|
||||
log.error("Kodi web server not enabled, can not cache images")
|
||||
return
|
||||
|
||||
# get the port
|
||||
web_port = {"setting": "services.webserverport"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_port)
|
||||
xbmc_port = result['result']['value']
|
||||
log.debug("xbmc_port: {0}", xbmc_port)
|
||||
xbmc_port = get_value("services.webserverport")
|
||||
log.debug("xbmc_port: {0}".format(xbmc_port))
|
||||
|
||||
# get the user
|
||||
web_user = {"setting": "services.webserverusername"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_user)
|
||||
xbmc_username = result['result']['value']
|
||||
log.debug("xbmc_username: {0}", xbmc_username)
|
||||
xbmc_username = get_value("services.webserverusername")
|
||||
log.debug("xbmc_username: {0}".format(xbmc_username))
|
||||
|
||||
# get the password
|
||||
web_pass = {"setting": "services.webserverpassword"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_pass)
|
||||
xbmc_password = result['result']['value']
|
||||
xbmc_password = get_value("services.webserverpassword")
|
||||
|
||||
progress.update(0, string_load(30356))
|
||||
|
||||
params = {"properties": ["url"]}
|
||||
json_result = JsonRpc('Textures.GetTextures').execute(params)
|
||||
textures = json_result.get("result", {}).get("textures", [])
|
||||
log.debug("Textures.GetTextures Count: {0}", len(textures))
|
||||
log.debug("Textures.GetTextures Count: {0}".format(len(textures)))
|
||||
|
||||
if self.stop_all_activity:
|
||||
return
|
||||
@@ -293,7 +283,7 @@ class CacheArtwork(threading.Thread):
|
||||
texture_urls = set()
|
||||
for texture in textures:
|
||||
url = texture.get("url")
|
||||
url = urllib.unquote(url)
|
||||
url = unquote(url)
|
||||
url = url.replace("image://", "")
|
||||
url = url[0:-1]
|
||||
texture_urls.add(url)
|
||||
@@ -301,7 +291,7 @@ class CacheArtwork(threading.Thread):
|
||||
del textures
|
||||
del json_result
|
||||
|
||||
log.debug("texture_urls Count: {0}", len(texture_urls))
|
||||
log.debug("texture_urls Count: {0}".format(len(texture_urls)))
|
||||
|
||||
if self.stop_all_activity:
|
||||
return
|
||||
@@ -313,7 +303,7 @@ class CacheArtwork(threading.Thread):
|
||||
return
|
||||
|
||||
missing_texture_urls = set()
|
||||
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
|
||||
|
||||
for image_url in jellyfin_texture_urls:
|
||||
if image_url not in texture_urls and not image_url.endswith("&Tag=") and len(image_url) > 0:
|
||||
missing_texture_urls.add(image_url)
|
||||
@@ -321,10 +311,10 @@ class CacheArtwork(threading.Thread):
|
||||
if self.stop_all_activity:
|
||||
return
|
||||
|
||||
log.debug("texture_urls: {0}", texture_urls)
|
||||
log.debug("missing_texture_urls: {0}", missing_texture_urls)
|
||||
log.debug("Number of existing textures: {0}", len(texture_urls))
|
||||
log.debug("Number of missing textures: {0}", len(missing_texture_urls))
|
||||
log.debug("texture_urls: {0}".format(texture_urls))
|
||||
log.debug("missing_texture_urls: {0}".format(missing_texture_urls))
|
||||
log.debug("Number of existing textures: {0}".format(len(texture_urls)))
|
||||
log.debug("Number of missing textures: {0}".format(len(missing_texture_urls)))
|
||||
|
||||
kodi_http_server = "localhost:" + str(xbmc_port)
|
||||
headers = {}
|
||||
@@ -333,29 +323,23 @@ class CacheArtwork(threading.Thread):
|
||||
headers = {'Authorization': 'Basic %s' % base64.b64encode(auth)}
|
||||
|
||||
total = len(missing_texture_urls)
|
||||
index = 1
|
||||
|
||||
count_done = 0
|
||||
for get_url in missing_texture_urls:
|
||||
# log.debug("texture_url: {0}", get_url)
|
||||
url = double_urlencode(get_url)
|
||||
kodi_texture_url = ("/image/image://%s" % url)
|
||||
log.debug("kodi_texture_url: {0}", kodi_texture_url)
|
||||
for index, get_url in enumerate(missing_texture_urls, 1):
|
||||
kodi_texture_url = "/image/image://{0}".format(get_url)
|
||||
log.debug("kodi_texture_url: {0}".format(kodi_texture_url))
|
||||
|
||||
percentage = int((float(index) / float(total)) * 100)
|
||||
message = "%s of %s" % (index, total)
|
||||
progress.update(percentage, message)
|
||||
|
||||
conn = httplib.HTTPConnection(kodi_http_server, timeout=20)
|
||||
conn.request(method="GET", url=kodi_texture_url, headers=headers)
|
||||
data = conn.getresponse()
|
||||
if data.status == 200:
|
||||
count_done += 1
|
||||
log.debug("Get Image Result: {0}", data.status)
|
||||
cache_url = "http://%s%s" % (kodi_http_server, kodi_texture_url)
|
||||
data = requests.get(cache_url, timeout=20, headers=headers)
|
||||
|
||||
if data.status_code == 200:
|
||||
count_done += 1
|
||||
log.debug("Get Image Result: {0}".format(data.status_code))
|
||||
|
||||
index += 1
|
||||
# if progress.iscanceled():
|
||||
# if "iscanceled" in dir(progress) and progress.iscanceled():
|
||||
if isinstance(progress, xbmcgui.DialogProgress) and progress.iscanceled():
|
||||
break
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
from uuid import uuid4 as uuid4
|
||||
from uuid import uuid4
|
||||
from kodi_six.utils import py2_decode
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ClientInformation:
|
||||
@@ -22,21 +24,22 @@ class ClientInformation:
|
||||
if client_id:
|
||||
return client_id
|
||||
|
||||
jellyfin_guid_path = xbmc.translatePath("special://temp/jellycon_guid").decode('utf-8')
|
||||
log.debug("jellyfin_guid_path: {0}", jellyfin_guid_path)
|
||||
jellyfin_guid_path = py2_decode(xbmc.translatePath("special://temp/jellycon_guid"))
|
||||
log.debug("jellyfin_guid_path: {0}".format(jellyfin_guid_path))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path)
|
||||
client_id = guid.read()
|
||||
guid.close()
|
||||
|
||||
if not client_id:
|
||||
client_id = str("%012X" % uuid4())
|
||||
log.debug("Generating a new guid: {0}", client_id)
|
||||
# Needs to be captilized for backwards compat
|
||||
client_id = uuid4().hex.upper()
|
||||
log.debug("Generating a new guid: {0}".format(client_id))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
|
||||
guid.write(client_id)
|
||||
guid.close()
|
||||
log.debug("jellyfin_client_id (NEW): {0}", client_id)
|
||||
log.debug("jellyfin_client_id (NEW): {0}".format(client_id))
|
||||
else:
|
||||
log.debug("jellyfin_client_id: {0}", client_id)
|
||||
log.debug("jellyfin_client_id: {0}".format(client_id))
|
||||
|
||||
window.set_property("client_id", client_id)
|
||||
return client_id
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
import threading
|
||||
import xbmc
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from resources.lib.functions import show_menu
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ContextMonitor(threading.Thread):
|
||||
@@ -16,7 +17,7 @@ class ContextMonitor(threading.Thread):
|
||||
item_id = None
|
||||
log.debug("ContextMonitor Thread Started")
|
||||
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
|
||||
xbmc.sleep(1000)
|
||||
@@ -33,41 +34,6 @@ class ContextMonitor(threading.Thread):
|
||||
|
||||
xbmc.sleep(100)
|
||||
|
||||
'''
|
||||
context_up = False
|
||||
is_jellycon_item = False
|
||||
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
|
||||
xbmc.sleep(1000)
|
||||
else:
|
||||
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
|
||||
context_up = True
|
||||
if is_jellycon_item:
|
||||
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
|
||||
else:
|
||||
if context_up: # context now down, do something
|
||||
context_up = False
|
||||
container_id = xbmc.getInfoLabel("System.CurrentControlID")
|
||||
log.debug("ContextMonitor Container ID: {0}", container_id)
|
||||
item_id = xbmc.getInfoLabel("Container(" + str(container_id) + ").ListItem.Property(id)")
|
||||
log.debug("ContextMonitor Item ID: {0}", item_id)
|
||||
if item_id:
|
||||
params = {}
|
||||
params["item_id"] = item_id
|
||||
show_menu(params)
|
||||
|
||||
container_id = xbmc.getInfoLabel("System.CurrentControlID")
|
||||
condition = ("String.StartsWith(Container(" + str(container_id) +
|
||||
").ListItem.Path,plugin://plugin.video.jellycon) + !String.IsEmpty(Container(" +
|
||||
str(container_id) + ").ListItem.Property(id))")
|
||||
is_jellycon_item = xbmc.getCondVisibility(condition)
|
||||
|
||||
xbmc.sleep(200)
|
||||
|
||||
'''
|
||||
|
||||
log.debug("ContextMonitor Thread Exited")
|
||||
|
||||
def stop_monitor(self):
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
import hashlib
|
||||
import os
|
||||
import cPickle
|
||||
import time
|
||||
from six.moves import cPickle
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .item_functions import extract_item_info
|
||||
from .kodi_utils import HomeWindow
|
||||
from .translation import string_load
|
||||
@@ -21,7 +21,7 @@ import xbmcaddon
|
||||
import xbmcvfs
|
||||
import xbmcgui
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class CacheItem:
|
||||
@@ -46,21 +46,15 @@ class DataManager:
|
||||
# log.debug("DataManager __init__")
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def load_json_data(json_data):
|
||||
return json.loads(json_data, object_hook=lambda d: defaultdict(lambda: None, d))
|
||||
|
||||
@timer
|
||||
def get_content(self, url):
|
||||
json_data = DownloadUtils().download_url(url)
|
||||
result = self.load_json_data(json_data)
|
||||
return result
|
||||
return DownloadUtils().download_url(url)
|
||||
|
||||
@timer
|
||||
def get_items(self, url, gui_options, use_cache=False):
|
||||
|
||||
home_window = HomeWindow()
|
||||
log.debug("last_content_url : use_cache={0} url={1}", use_cache, url)
|
||||
log.debug("last_content_url : use_cache={0} url={1}".format(use_cache, url))
|
||||
home_window.set_property("last_content_url", url)
|
||||
|
||||
download_utils = DownloadUtils()
|
||||
@@ -68,7 +62,7 @@ class DataManager:
|
||||
server = download_utils.get_server()
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(user_id + "|" + str(server) + "|" + url)
|
||||
m.update('{}|{}|{}'.format(user_id, server, url).encode())
|
||||
url_hash = m.hexdigest()
|
||||
cache_file = os.path.join(self.addon_dir, "cache_" + url_hash + ".pickle")
|
||||
|
||||
@@ -102,7 +96,7 @@ class DataManager:
|
||||
item_list = cache_item.item_list
|
||||
total_records = cache_item.total_records
|
||||
except Exception as err:
|
||||
log.error("Pickle Data Load Failed : {0}", err)
|
||||
log.error("Pickle Data Load Failed : {0}".format(err))
|
||||
item_list = None
|
||||
|
||||
# we need to load the list item data form the server
|
||||
@@ -206,7 +200,7 @@ class CacheManagerThread(threading.Thread):
|
||||
else:
|
||||
log.debug("CacheManagerThread : Reloading to recheck data hashes")
|
||||
cached_hash = self.cached_item.item_list_hash
|
||||
log.debug("CacheManagerThread : Cache Hash : {0}", cached_hash)
|
||||
log.debug("CacheManagerThread : Cache Hash : {0}".format(cached_hash))
|
||||
|
||||
data_manager = DataManager()
|
||||
results = data_manager.get_content(self.cached_item.items_url)
|
||||
@@ -232,7 +226,7 @@ class CacheManagerThread(threading.Thread):
|
||||
return
|
||||
|
||||
loaded_hash = self.get_data_hash(loaded_items)
|
||||
log.debug("CacheManagerThread : Loaded Hash : {0}", loaded_hash)
|
||||
log.debug("CacheManagerThread : Loaded Hash : {0}".format(loaded_hash))
|
||||
|
||||
# if they dont match then save the data and trigger a content reload
|
||||
if cached_hash != loaded_hash:
|
||||
@@ -252,7 +246,7 @@ class CacheManagerThread(threading.Thread):
|
||||
# TODO: probably should only set this in simple check mode
|
||||
current_time_stamp = str(time.time())
|
||||
home_window.set_property("jellycon_widget_reload", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
|
||||
|
||||
log.debug("CacheManagerThread : Sending container refresh")
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
@@ -276,11 +270,12 @@ def clear_cached_server_data():
|
||||
del_count = 0
|
||||
for filename in files:
|
||||
if filename.startswith("cache_") and filename.endswith(".pickle"):
|
||||
log.debug("Deleteing CacheFile: {0}", filename)
|
||||
log.debug("Deleteing CacheFile: {0}".format(filename))
|
||||
xbmcvfs.delete(os.path.join(addon_dir, filename))
|
||||
del_count += 1
|
||||
|
||||
msg = string_load(30394) % del_count
|
||||
log.debug('Deleted {} files'.format(del_count))
|
||||
msg = string_load(30394)
|
||||
xbmcgui.Dialog().ok(string_load(30393), msg)
|
||||
|
||||
|
||||
@@ -293,7 +288,7 @@ def clear_old_cache_data():
|
||||
del_count = 0
|
||||
for filename in files:
|
||||
if filename.startswith("cache_") and filename.endswith(".pickle"):
|
||||
log.debug("clear_old_cache_data() : Checking CacheFile : {0}", filename)
|
||||
log.debug("clear_old_cache_data() : Checking CacheFile : {0}".format(filename))
|
||||
|
||||
cache_item = None
|
||||
for x in range(0, 5):
|
||||
@@ -304,7 +299,7 @@ def clear_old_cache_data():
|
||||
cache_item = cPickle.load(handle)
|
||||
break
|
||||
except Exception as error:
|
||||
log.debug("clear_old_cache_data() : Pickle load error : {0}", error)
|
||||
log.debug("clear_old_cache_data() : Pickle load error : {0}".format(error))
|
||||
cache_item = None
|
||||
xbmc.sleep(1000)
|
||||
|
||||
@@ -313,9 +308,9 @@ def clear_old_cache_data():
|
||||
if cache_item.date_last_used is not None:
|
||||
item_last_used = time.time() - cache_item.date_last_used
|
||||
|
||||
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago", item_last_used)
|
||||
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago".format(item_last_used))
|
||||
if item_last_used == -1 or item_last_used > (3600 * 24 * 7):
|
||||
log.debug("clear_old_cache_data() : Deleting cache item age : {0}", item_last_used)
|
||||
log.debug("clear_old_cache_data() : Deleting cache item age : {0}".format(item_last_used))
|
||||
data_file = os.path.join(addon_dir, filename)
|
||||
with FileLock(data_file + ".locked", timeout=5):
|
||||
xbmcvfs.delete(data_file)
|
||||
@@ -326,4 +321,4 @@ def clear_old_cache_data():
|
||||
with FileLock(data_file + ".locked", timeout=5):
|
||||
xbmcvfs.delete(data_file)
|
||||
|
||||
log.debug("clear_old_cache_data() : Cache items deleted : {0}", del_count)
|
||||
log.debug("clear_old_cache_data() : Cache items deleted : {0}".format(del_count))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote, unquote
|
||||
import sys
|
||||
import re
|
||||
|
||||
@@ -12,12 +13,12 @@ from .datamanager import DataManager
|
||||
from .kodi_utils import HomeWindow
|
||||
from .downloadutils import DownloadUtils
|
||||
from .translation import string_load
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .item_functions import add_gui_item, ItemDetails
|
||||
from .utils import send_event_notification
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
@timer
|
||||
@@ -29,8 +30,8 @@ def get_content(url, params):
|
||||
if not media_type:
|
||||
xbmcgui.Dialog().ok(string_load(30135), string_load(30139))
|
||||
|
||||
log.debug("URL: {0}", url)
|
||||
log.debug("MediaType: {0}", media_type)
|
||||
log.debug("URL: {0}".format(url))
|
||||
log.debug("MediaType: {0}".format(media_type))
|
||||
pluginhandle = int(sys.argv[1])
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -71,7 +72,7 @@ def get_content(url, params):
|
||||
elif media_type == "playlists":
|
||||
view_type = "Playlists"
|
||||
|
||||
log.debug("media_type:{0} content_type:{1} view_type:{2} ", media_type, content_type, view_type)
|
||||
log.debug("media_type:{0} content_type:{1} view_type:{2} ".format(media_type, content_type, view_type))
|
||||
|
||||
# show a progress indicator if needed
|
||||
progress = None
|
||||
@@ -88,22 +89,22 @@ def get_content(url, params):
|
||||
if page_limit > 0 and media_type.startswith("movie"):
|
||||
m = re.search('StartIndex=([0-9]{1,4})', url)
|
||||
if m and m.group(1):
|
||||
log.debug("UPDATING NEXT URL: {0}", url)
|
||||
log.debug("UPDATING NEXT URL: {0}".format(url))
|
||||
start_index = int(m.group(1))
|
||||
log.debug("current_start : {0}", start_index)
|
||||
log.debug("current_start : {0}".format(start_index))
|
||||
if start_index > 0:
|
||||
prev_index = start_index - page_limit
|
||||
if prev_index < 0:
|
||||
prev_index = 0
|
||||
url_prev = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(prev_index), url)
|
||||
url_next = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(start_index + page_limit), url)
|
||||
log.debug("UPDATING NEXT URL: {0}", url_next)
|
||||
log.debug("UPDATING NEXT URL: {0}".format(url_next))
|
||||
|
||||
else:
|
||||
log.debug("ADDING NEXT URL: {0}", url)
|
||||
log.debug("ADDING NEXT URL: {0}".format(url))
|
||||
url_next = url + "&StartIndex=" + str(start_index + page_limit) + "&Limit=" + str(page_limit)
|
||||
url = url + "&StartIndex=" + str(start_index) + "&Limit=" + str(page_limit)
|
||||
log.debug("ADDING NEXT URL: {0}", url_next)
|
||||
log.debug("ADDING NEXT URL: {0}".format(url_next))
|
||||
|
||||
# use the data manager to get the data
|
||||
# result = dataManager.GetContent(url)
|
||||
@@ -118,15 +119,15 @@ def get_content(url, params):
|
||||
if dir_items is None:
|
||||
return
|
||||
|
||||
log.debug("total_records: {0}", total_records)
|
||||
log.debug("total_records: {0}".format(total_records))
|
||||
|
||||
# add paging items
|
||||
if page_limit > 0 and media_type.startswith("movie"):
|
||||
if url_prev:
|
||||
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - page_limit + 1) + "-" + str(start_index) +
|
||||
" of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}", u, list_item)
|
||||
u = sys.argv[0] + "?url=" + quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.insert(0, (u, list_item, True))
|
||||
|
||||
if start_index + page_limit < total_records:
|
||||
@@ -135,8 +136,8 @@ def get_content(url, params):
|
||||
upper_count = total_records
|
||||
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + page_limit + 1) + "-" +
|
||||
str(upper_count) + " of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}", u, list_item)
|
||||
u = sys.argv[0] + "?url=" + quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.append((u, list_item, True))
|
||||
|
||||
# set the Kodi content type
|
||||
@@ -144,7 +145,7 @@ def get_content(url, params):
|
||||
xbmcplugin.setContent(pluginhandle, content_type)
|
||||
elif detected_type is not None:
|
||||
# if the media type is not set then try to use the detected type
|
||||
log.debug("Detected content type: {0}", detected_type)
|
||||
log.debug("Detected content type: {0}".format(detected_type))
|
||||
if detected_type == "Movie":
|
||||
view_type = "Movies"
|
||||
content_type = 'movies'
|
||||
@@ -166,16 +167,11 @@ def get_content(url, params):
|
||||
view_key = "view-" + content_type
|
||||
view_id = settings.getSetting(view_key)
|
||||
if view_id:
|
||||
log.debug("Setting view for type:{0} to id:{1}", view_key, view_id)
|
||||
log.debug("Setting view for type:{0} to id:{1}".format(view_key, view_id))
|
||||
display_items_notification = {"view_id": view_id}
|
||||
send_event_notification("set_view", display_items_notification)
|
||||
else:
|
||||
log.debug("No view id for view type:{0}", view_key)
|
||||
|
||||
# send display items event
|
||||
# display_items_notification = {"view_type": view_type}
|
||||
# log.debug("Sending display_items with data {0}", display_items_notification)
|
||||
# send_event_notification("display_items", display_items_notification)
|
||||
log.debug("No view id for view type:{0}".format(view_key))
|
||||
|
||||
if progress is not None:
|
||||
progress.update(100, string_load(30125))
|
||||
@@ -185,7 +181,7 @@ def get_content(url, params):
|
||||
|
||||
|
||||
def set_sort(pluginhandle, view_type, default_sort):
|
||||
log.debug("SETTING_SORT for media type: {0}", view_type)
|
||||
log.debug("SETTING_SORT for media type: {0}".format(view_type))
|
||||
|
||||
if default_sort == "none":
|
||||
xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
@@ -202,7 +198,7 @@ def set_sort(pluginhandle, view_type, default_sort):
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
preset_sort_order = settings.getSetting("sort-" + view_type)
|
||||
log.debug("SETTING_SORT preset_sort_order: {0}", preset_sort_order)
|
||||
log.debug("SETTING_SORT preset_sort_order: {0}".format(preset_sort_order))
|
||||
if preset_sort_order in sorting_order_mapping:
|
||||
xbmcplugin.addSortMethod(pluginhandle, sorting_order_mapping[preset_sort_order])
|
||||
|
||||
@@ -237,7 +233,7 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
name_format = params.get("name_format", None)
|
||||
name_format_type = None
|
||||
if name_format is not None:
|
||||
name_format = urllib.unquote(name_format)
|
||||
name_format = unquote(name_format)
|
||||
tokens = name_format.split("|")
|
||||
if len(tokens) == 2:
|
||||
name_format_type = tokens[0]
|
||||
@@ -311,7 +307,7 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
detected_type = item_details.item_type
|
||||
|
||||
if item_details.item_type == "Season" and first_season_item is None:
|
||||
log.debug("Setting First Season to : {0}", item_details.__dict__)
|
||||
log.debug("Setting First Season to : {0}".format(item_details.__dict__))
|
||||
first_season_item = item_details
|
||||
|
||||
total_unwatched += item_details.unwatched_episodes
|
||||
@@ -357,7 +353,7 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
if gui_item:
|
||||
dir_items.append(gui_item)
|
||||
else:
|
||||
log.debug("Dropping empty folder item : {0}", item_details.__dict__)
|
||||
log.debug("Dropping empty folder item : {0}".format(item_details.__dict__))
|
||||
|
||||
elif item_details.item_type == "MusicArtist":
|
||||
u = ('{server}/Users/{userid}/items' +
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
import httplib
|
||||
import requests
|
||||
import hashlib
|
||||
import ssl
|
||||
import StringIO
|
||||
import gzip
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
import urllib
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from base64 import b64encode
|
||||
from collections import defaultdict
|
||||
from traceback import format_exc
|
||||
from kodi_six.utils import py2_decode
|
||||
from six import ensure_text
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .clientinfo import ClientInformation
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .translation import string_load
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def save_user_details(settings, user_name, user_password):
|
||||
@@ -104,16 +106,15 @@ class DownloadUtils:
|
||||
self.use_https = False
|
||||
if settings.getSetting('protocol') == "1":
|
||||
self.use_https = True
|
||||
log.debug("use_https: {0}", self.use_https)
|
||||
log.debug("use_https: {0}".format(self.use_https))
|
||||
|
||||
self.verify_cert = settings.getSetting('verify_cert') == 'true'
|
||||
log.debug("verify_cert: {0}", self.verify_cert)
|
||||
log.debug("verify_cert: {0}".format(self.verify_cert))
|
||||
|
||||
def post_capabilities(self):
|
||||
|
||||
url = "{server}/Sessions/Capabilities/Full?format=json"
|
||||
data = {
|
||||
'IconUrl': "https://raw.githubusercontent.com/faush01/plugin.video.jellycon/develop/kodi.png",
|
||||
'SupportsMediaControl': True,
|
||||
'PlayableMediaTypes': ["Video", "Audio"],
|
||||
'SupportedCommands': ["MoveUp",
|
||||
@@ -151,13 +152,12 @@ class DownloadUtils:
|
||||
}
|
||||
|
||||
self.download_url(url, post_body=data, method="POST")
|
||||
log.debug("Posted Capabilities: {0}", data)
|
||||
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")
|
||||
@@ -332,60 +332,24 @@ class DownloadUtils:
|
||||
else:
|
||||
url = "{server}/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s" % (item_id, bitrate)
|
||||
|
||||
log.debug("PlaybackInfo : {0}", url)
|
||||
log.debug("PlaybackInfo : {0}", profile)
|
||||
log.debug("PlaybackInfo : {0}".format(url))
|
||||
log.debug("PlaybackInfo : {0}".format(profile))
|
||||
play_info_result = self.download_url(url, post_body=playback_info, method="POST")
|
||||
play_info_result = json.loads(play_info_result)
|
||||
log.debug("PlaybackInfo : {0}", play_info_result)
|
||||
log.debug("PlaybackInfo : {0}".format(play_info_result))
|
||||
|
||||
return play_info_result
|
||||
|
||||
def get_server(self):
|
||||
settings = xbmcaddon.Addon()
|
||||
host = settings.getSetting('ipaddress')
|
||||
|
||||
if len(host) == 0 or host == "<none>":
|
||||
return None
|
||||
#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', "")
|
||||
|
||||
port = settings.getSetting('port')
|
||||
|
||||
if not port and self.use_https:
|
||||
port = "443"
|
||||
settings.setSetting("port", port)
|
||||
elif not port:
|
||||
port = "80"
|
||||
settings.setSetting("port", port)
|
||||
|
||||
# if user entered a full path i.e. http://some_host:port
|
||||
if host.lower().strip().startswith("http://") or host.lower().strip().startswith("https://"):
|
||||
log.debug("Extracting host info from url: {0}", host)
|
||||
url_bits = urlparse(host.strip())
|
||||
|
||||
if host.lower().strip().startswith("http://"):
|
||||
settings.setSetting('protocol', '0')
|
||||
self.use_https = False
|
||||
elif host.lower().strip().startswith("https://"):
|
||||
settings.setSetting('protocol', '1')
|
||||
self.use_https = True
|
||||
|
||||
if url_bits.hostname is not None and len(url_bits.hostname) > 0:
|
||||
host = url_bits.hostname
|
||||
|
||||
if url_bits.username and url_bits.password:
|
||||
host = "%s:%s@" % (url_bits.username, url_bits.password) + host
|
||||
|
||||
settings.setSetting("ipaddress", host)
|
||||
|
||||
if url_bits.port is not None and url_bits.port > 0:
|
||||
port = str(url_bits.port)
|
||||
settings.setSetting("port", port)
|
||||
|
||||
if self.use_https:
|
||||
server = "https://" + host + ":" + port
|
||||
else:
|
||||
server = "http://" + host + ":" + port
|
||||
|
||||
return server
|
||||
return settings.getSetting('server_address')
|
||||
|
||||
@staticmethod
|
||||
def get_all_artwork(item, server):
|
||||
@@ -394,7 +358,6 @@ class DownloadUtils:
|
||||
item_id = item["Id"]
|
||||
item_type = item["Type"]
|
||||
image_tags = item["ImageTags"]
|
||||
# bg_item_tags = item["ParentBackdropImageTags"]
|
||||
|
||||
# All the image tags
|
||||
for tag_name in image_tags:
|
||||
@@ -422,31 +385,28 @@ class DownloadUtils:
|
||||
item_id = data["SeriesId"]
|
||||
|
||||
image_tag = ""
|
||||
# "e3ab56fe27d389446754d0fb04910a34" # a place holder tag, needs to be in this format
|
||||
|
||||
# for episodes always use the parent BG
|
||||
if item_type == "Episode" and art_type == "Backdrop":
|
||||
item_id = data["ParentBackdropItemId"]
|
||||
bg_item_tags = data["ParentBackdropImageTags"]
|
||||
if bg_item_tags is not None and len(bg_item_tags) > 0:
|
||||
item_id = data.get("ParentBackdropItemId")
|
||||
bg_item_tags = data.get("ParentBackdropImageTags", [])
|
||||
if bg_item_tags:
|
||||
image_tag = bg_item_tags[0]
|
||||
elif art_type == "Backdrop" and parent is True:
|
||||
item_id = data["ParentBackdropItemId"]
|
||||
bg_item_tags = data["ParentBackdropImageTags"]
|
||||
if bg_item_tags is not None and len(bg_item_tags) > 0:
|
||||
item_id = data.get("ParentBackdropItemId")
|
||||
bg_item_tags = data.get("ParentBackdropImageTags", [])
|
||||
if bg_item_tags:
|
||||
image_tag = bg_item_tags[0]
|
||||
elif art_type == "Backdrop":
|
||||
bg_tags = data["BackdropImageTags"]
|
||||
if bg_tags is not None and len(bg_tags) > index:
|
||||
bg_tags = data.get("BackdropImageTags", [])
|
||||
if bg_tags:
|
||||
image_tag = bg_tags[index]
|
||||
# log.debug("Background Image Tag: {0}", imageTag)
|
||||
elif parent is False:
|
||||
image_tags = data["ImageTags"]
|
||||
if image_tags is not None:
|
||||
image_tag_type = image_tags[art_type]
|
||||
if image_tag_type is not None:
|
||||
image_tags = data.get("ImageTags", [])
|
||||
if image_tags:
|
||||
image_tag_type = image_tags.get(art_type)
|
||||
if image_tag_type:
|
||||
image_tag = image_tag_type
|
||||
# log.debug("Image Tag: {0}", imageTag)
|
||||
elif parent is True:
|
||||
if (item_type == "Episode" or item_type == "Season") and art_type == 'Primary':
|
||||
tag_name = 'SeriesPrimaryImageTag'
|
||||
@@ -454,16 +414,14 @@ class DownloadUtils:
|
||||
else:
|
||||
tag_name = 'Parent%sImageTag' % art_type
|
||||
id_name = 'Parent%sItemId' % art_type
|
||||
parent_image_id = data[id_name]
|
||||
parent_image_tag = data[tag_name]
|
||||
parent_image_id = data.get(id_name)
|
||||
parent_image_tag = data.get(tag_name)
|
||||
if parent_image_id is not None and parent_image_tag is not None:
|
||||
item_id = parent_image_id
|
||||
image_tag = parent_image_tag
|
||||
# log.debug("Parent Image Tag: {0}", imageTag)
|
||||
|
||||
# ParentTag not passed for Banner and Art
|
||||
if not image_tag and not ((art_type == 'Banner' or art_type == 'Art') and parent is True):
|
||||
# log.debug("No Image Tag for request:{0} item:{1} parent:{2}", art_type, item_type, parent)
|
||||
return ""
|
||||
|
||||
artwork = "%s/Items/%s/Images/%s/%s?Format=original&Tag=%s" % (server, item_id, art_type, index, image_tag)
|
||||
@@ -471,19 +429,9 @@ class DownloadUtils:
|
||||
if self.use_https and not self.verify_cert:
|
||||
artwork += "|verifypeer=false"
|
||||
|
||||
# log.debug("getArtwork: request:{0} item:{1} parent:{2} link:{3}", art_type, item_type, parent, artwork)
|
||||
|
||||
'''
|
||||
# do not return non-existing images
|
||||
if ( (art_type != "Backdrop" and imageTag == "") |
|
||||
(art_type == "Backdrop" and data.get("BackdropImageTags") != None and len(data.get("BackdropImageTags")) == 0) |
|
||||
(art_type == "Backdrop" and data.get("BackdropImageTag") != None and len(data.get("BackdropImageTag")) == 0)
|
||||
):
|
||||
artwork = ''
|
||||
'''
|
||||
|
||||
return artwork
|
||||
|
||||
|
||||
def image_url(self, item_id, art_type, index, width, height, image_tag, server):
|
||||
|
||||
# test imageTag e3ab56fe27d389446754d0fb04910a34
|
||||
@@ -519,8 +467,8 @@ class DownloadUtils:
|
||||
userid = window.get_property("userid")
|
||||
user_image = window.get_property("userimage")
|
||||
|
||||
if userid and user_image:
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved UserID: {0}", userid)
|
||||
if userid:
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved UserID: {0}".format(userid))
|
||||
return userid
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -529,33 +477,27 @@ class DownloadUtils:
|
||||
|
||||
if not user_name:
|
||||
return ""
|
||||
log.debug("Looking for user name: {0}", user_name)
|
||||
log.debug("Looking for user name: {0}".format(user_name))
|
||||
|
||||
try:
|
||||
json_data = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
|
||||
result = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
|
||||
except Exception as msg:
|
||||
log.error("Get User unable to connect: {0}", msg)
|
||||
log.error("Get User unable to connect: {0}".format(msg))
|
||||
return ""
|
||||
|
||||
log.debug("GETUSER_JSONDATA_01: {0}", json_data)
|
||||
log.debug("GETUSER_JSONDATA_01: {0}".format(py2_decode(result)))
|
||||
|
||||
try:
|
||||
result = json.loads(json_data)
|
||||
except Exception as e:
|
||||
log.debug("Could not load user data: {0}", e)
|
||||
if not result:
|
||||
return ""
|
||||
|
||||
if result is None:
|
||||
return ""
|
||||
|
||||
log.debug("GETUSER_JSONDATA_02: {0}", result)
|
||||
log.debug("GETUSER_JSONDATA_02: {0}".format(result))
|
||||
|
||||
secure = False
|
||||
for user in result:
|
||||
if user.get("Name") == unicode(user_name, "utf-8"):
|
||||
if user.get("Name") == ensure_text(user_name, "utf-8"):
|
||||
userid = user.get("Id")
|
||||
user_image = self.get_user_artwork(user, 'Primary')
|
||||
log.debug("Username Found: {0}", user.get("Name"))
|
||||
log.debug("Username Found: {0}".format(user.get("Name")))
|
||||
if user.get("HasPassword", False):
|
||||
secure = True
|
||||
log.debug("Username Is Secure (HasPassword=True)")
|
||||
@@ -579,7 +521,7 @@ class DownloadUtils:
|
||||
string_load(30045),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
|
||||
log.debug("userid: {0}", userid)
|
||||
log.debug("userid: {0}".format(userid))
|
||||
|
||||
window.set_property("userid", userid)
|
||||
window.set_property("userimage", user_image)
|
||||
@@ -592,42 +534,33 @@ class DownloadUtils:
|
||||
|
||||
token = window.get_property("AccessToken")
|
||||
if token is not None and token != "":
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved AccessToken: {0}", token)
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved AccessToken: {0}".format(token))
|
||||
return token
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
port = settings.getSetting("port")
|
||||
host = settings.getSetting("ipaddress")
|
||||
if host is None or host == "" or port is None or port == "":
|
||||
return ""
|
||||
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", ""))
|
||||
user_name = user_details.get("username", "")
|
||||
pwd_text = user_details.get("password", "")
|
||||
|
||||
message_data = "username=" + user_name + "&pw=" + pwd_text
|
||||
message_data = {'username': user_name, 'pw': pwd_text}
|
||||
|
||||
resp = self.download_url(url, post_body=message_data, method="POST", suppress=True, authenticate=False)
|
||||
log.debug("AuthenticateByName: {0}", resp)
|
||||
result = self.download_url(url, post_body=message_data, method="POST", suppress=True, authenticate=False)
|
||||
log.debug("AuthenticateByName: {0}".format(result))
|
||||
|
||||
access_token = None
|
||||
userid = None
|
||||
try:
|
||||
result = json.loads(resp)
|
||||
access_token = result.get("AccessToken")
|
||||
# userid = result["SessionInfo"].get("UserId")
|
||||
userid = result["User"].get("Id")
|
||||
except:
|
||||
pass
|
||||
access_token = result.get("AccessToken")
|
||||
userid = result["User"].get("Id")
|
||||
|
||||
if access_token is not None:
|
||||
log.debug("User Authenticated: {0}", access_token)
|
||||
log.debug("User Id: {0}", userid)
|
||||
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()
|
||||
|
||||
@@ -648,7 +581,7 @@ class DownloadUtils:
|
||||
settings = xbmcaddon.Addon()
|
||||
device_name = settings.getSetting('deviceName')
|
||||
# remove none ascii chars
|
||||
device_name = device_name.decode("ascii", errors='ignore')
|
||||
device_name = py2_decode(device_name)
|
||||
# remove some chars not valid for names
|
||||
device_name = device_name.replace("\"", "_")
|
||||
if len(device_name) == 0:
|
||||
@@ -660,27 +593,24 @@ class DownloadUtils:
|
||||
|
||||
if authenticate is False:
|
||||
auth_string = "MediaBrowser Client=\"" + client + "\",Device=\"" + device_name + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\""
|
||||
# headers["Authorization"] = authString
|
||||
headers['X-Emby-Authorization'] = auth_string
|
||||
return headers
|
||||
else:
|
||||
userid = self.get_user_id()
|
||||
auth_string = "MediaBrowser UserId=\"" + userid + "\",Client=\"" + client + "\",Device=\"" + device_name + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\""
|
||||
# headers["Authorization"] = authString
|
||||
headers['X-Emby-Authorization'] = auth_string
|
||||
|
||||
auth_token = self.authenticate()
|
||||
if auth_token != "":
|
||||
headers["X-MediaBrowser-Token"] = auth_token
|
||||
|
||||
log.debug("JellyCon Authentication Header: {0}", headers)
|
||||
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")
|
||||
|
||||
return_data = "null"
|
||||
settings = xbmcaddon.Addon()
|
||||
user_details = load_user_details(settings)
|
||||
username = user_details.get("username", "")
|
||||
@@ -689,23 +619,23 @@ class DownloadUtils:
|
||||
http_timeout = int(settings.getSetting("http_timeout"))
|
||||
|
||||
if authenticate and username == "":
|
||||
return return_data
|
||||
return {}
|
||||
|
||||
if settings.getSetting("suppressErrors") == "true":
|
||||
suppress = True
|
||||
|
||||
log.debug("Before: {0}", url)
|
||||
log.debug("Before: {0}".format(url))
|
||||
|
||||
if url.find("{server}") != -1:
|
||||
server = self.get_server()
|
||||
if server is None:
|
||||
return return_data
|
||||
return {}
|
||||
url = url.replace("{server}", server)
|
||||
|
||||
if url.find("{userid}") != -1:
|
||||
userid = self.get_user_id()
|
||||
if not userid:
|
||||
return return_data
|
||||
return {}
|
||||
url = url.replace("{userid}", userid)
|
||||
|
||||
if url.find("{ItemLimit}") != -1:
|
||||
@@ -720,120 +650,75 @@ class DownloadUtils:
|
||||
home_window = HomeWindow()
|
||||
random_movies = home_window.get_property("random-movies")
|
||||
if not random_movies:
|
||||
return return_data
|
||||
return {}
|
||||
url = url.replace("{random_movies}", random_movies)
|
||||
|
||||
log.debug("After: {0}", url)
|
||||
conn = None
|
||||
log.debug("After: {0}".format(url))
|
||||
|
||||
try:
|
||||
|
||||
url_bits = urlparse(url.strip())
|
||||
|
||||
protocol = url_bits.scheme
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_puery = url_bits.query
|
||||
|
||||
if not host_name or host_name == "<none>":
|
||||
return return_data
|
||||
|
||||
local_use_https = False
|
||||
if protocol.lower() == "https":
|
||||
local_use_https = True
|
||||
|
||||
server = "%s:%s" % (host_name, port)
|
||||
url_path = url_path + "?" + url_puery
|
||||
|
||||
if local_use_https and self.verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout)
|
||||
elif local_use_https and not self.verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert NOT checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout, context=ssl._create_unverified_context())
|
||||
else:
|
||||
log.debug("Connection: HTTP")
|
||||
conn = httplib.HTTPConnection(server, timeout=http_timeout)
|
||||
|
||||
head = self.get_auth_header(authenticate)
|
||||
|
||||
if user_name and user_password:
|
||||
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()
|
||||
log.debug("HEADERS: {0}", head)
|
||||
|
||||
if post_body is not None:
|
||||
http_request = getattr(requests, method.lower())
|
||||
|
||||
if post_body:
|
||||
|
||||
if isinstance(post_body, dict):
|
||||
content_type = "application/json"
|
||||
head["Content-Type"] = "application/json"
|
||||
post_body = json.dumps(post_body)
|
||||
else:
|
||||
content_type = "application/x-www-form-urlencoded"
|
||||
head["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
|
||||
head["Content-Type"] = content_type
|
||||
log.debug("Content-Type: {0}", content_type)
|
||||
log.debug("Content-Type: {0}".format(head["Content-Type"]))
|
||||
log.debug("POST DATA: {0}".format(post_body))
|
||||
|
||||
log.debug("POST DATA: {0}", post_body)
|
||||
conn.request(method=method, url=url_path, body=post_body, headers=head)
|
||||
data = http_request(url, data=post_body, headers=head)
|
||||
else:
|
||||
conn.request(method=method, url=url_path, headers=head)
|
||||
data = http_request(url, headers=head)
|
||||
|
||||
data = conn.getresponse()
|
||||
log.debug("HTTP response: {0} {1}", data.status, data.reason)
|
||||
log.debug("GET URL HEADERS: {0}", data.getheaders())
|
||||
|
||||
if int(data.status) == 200:
|
||||
ret_data = data.read()
|
||||
content_type = data.getheader('content-encoding')
|
||||
log.debug("Data Len Before: {0}", len(ret_data))
|
||||
if content_type == "gzip":
|
||||
ret_data = StringIO.StringIO(ret_data)
|
||||
gzipper = gzip.GzipFile(fileobj=ret_data)
|
||||
return_data = gzipper.read()
|
||||
else:
|
||||
return_data = ret_data
|
||||
if data.status_code == 200:
|
||||
|
||||
if headers is not None and isinstance(headers, dict):
|
||||
headers.update(data.getheaders())
|
||||
log.debug("Data Len After: {0}", len(return_data))
|
||||
log.debug("====== 200 returned =======")
|
||||
log.debug("Content-Type: {0}", content_type)
|
||||
log.debug("{0}", return_data)
|
||||
log.debug("====== 200 finished ======")
|
||||
headers.update(data.headers)
|
||||
log.debug("{0}".format(data.json()))
|
||||
|
||||
elif int(data.status) >= 400:
|
||||
elif data.status_code >= 400:
|
||||
|
||||
if int(data.status) == 401:
|
||||
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}", hashed_username)
|
||||
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: {0} {1}", data.status, data.reason)
|
||||
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.reason),
|
||||
'{}: {}'.format(string_load(30200), 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("Unable to connect to {0} : {1}", server, msg)
|
||||
if suppress is False:
|
||||
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")
|
||||
|
||||
finally:
|
||||
try:
|
||||
log.debug("Closing HTTP connection: {0}", conn)
|
||||
conn.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
return return_data
|
||||
|
||||
@@ -90,10 +90,6 @@ import sys
|
||||
import time
|
||||
import errno
|
||||
|
||||
# from .simple_logging import SimpleLogging
|
||||
# log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class FileLock(object):
|
||||
""" A file locking mechanism that has context-manager support so
|
||||
you can use it in a ``with`` statement. This should be relatively cross
|
||||
@@ -201,7 +197,7 @@ if __name__ == "__main__":
|
||||
import threading
|
||||
import tempfile
|
||||
from builtins import range
|
||||
|
||||
|
||||
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
protected_filepath = os.path.join(temp_dir, "somefile.txt")
|
||||
@@ -236,4 +232,4 @@ if __name__ == "__main__":
|
||||
# Please manually inspect the output. Does it look like the operations were atomic?
|
||||
with open(protected_filepath, "r") as f:
|
||||
sys.stdout.write(f.read())
|
||||
"""
|
||||
"""
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote, unquote, parse_qsl
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import cProfile
|
||||
import pstats
|
||||
import json
|
||||
import StringIO
|
||||
from six import StringIO
|
||||
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
@@ -20,7 +21,7 @@ from .kodi_utils import HomeWindow
|
||||
from .clientinfo import ClientInformation
|
||||
from .datamanager import DataManager, clear_cached_server_data
|
||||
from .server_detect import check_server, check_connection_speed
|
||||
from .simple_logging import SimpleLogging
|
||||
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 .server_sessions import show_server_sessions
|
||||
@@ -33,13 +34,14 @@ 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'))
|
||||
__cwd__ = __addon__.getAddonInfo('path')
|
||||
PLUGINPATH = xbmc.translatePath(os.path.join(__cwd__))
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
|
||||
@@ -60,20 +62,20 @@ def main_entry_point():
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
log.debug("Running Python: {0}", sys.version_info)
|
||||
log.debug("Running JellyCon: {0}", ClientInformation().get_version())
|
||||
log.debug("Kodi BuildVersion: {0}", xbmc.getInfoLabel("System.BuildVersion"))
|
||||
log.debug("Kodi Version: {0}", kodi_version)
|
||||
log.debug("Script argument data: {0}", sys.argv)
|
||||
log.debug("Running Python: {0}".format(sys.version_info))
|
||||
log.debug("Running JellyCon: {0}".format(ClientInformation().get_version()))
|
||||
log.debug("Kodi BuildVersion: {0}".format(xbmc.getInfoLabel("System.BuildVersion")))
|
||||
log.debug("Kodi Version: {0}".format(kodi_version))
|
||||
log.debug("Script argument data: {0}".format(sys.argv))
|
||||
|
||||
params = get_params()
|
||||
log.debug("Script params: {0}", params)
|
||||
log.info("Script params: {0}".format(params))
|
||||
|
||||
request_path = params.get("request_path", None)
|
||||
param_url = params.get('url', None)
|
||||
|
||||
if param_url:
|
||||
param_url = urllib.unquote(param_url)
|
||||
param_url = unquote(param_url)
|
||||
|
||||
mode = params.get("mode", None)
|
||||
|
||||
@@ -148,8 +150,8 @@ def main_entry_point():
|
||||
else:
|
||||
log.info("Unable to find TV show parent ID.")
|
||||
else:
|
||||
log.debug("JellyCon -> Mode: {0}", mode)
|
||||
log.debug("JellyCon -> URL: {0}", param_url)
|
||||
log.debug("JellyCon -> Mode: {0}".format(mode))
|
||||
log.debug("JellyCon -> URL: {0}".format(param_url))
|
||||
|
||||
if mode == "GET_CONTENT":
|
||||
get_content(param_url, params)
|
||||
@@ -189,7 +191,7 @@ def __get_parent_id_from(params):
|
||||
result = None
|
||||
show_provider_ids = params.get("show_ids")
|
||||
if show_provider_ids is not None:
|
||||
log.debug("TV show providers IDs: {}", show_provider_ids)
|
||||
log.debug("TV show providers IDs: {}".format(show_provider_ids))
|
||||
get_show_url = "{server}/Users/{userid}/Items?fields=MediaStreams&Recursive=true" \
|
||||
"&IncludeItemTypes=series&IncludeMedia=true&ImageTypeLimit=1&Limit=16" \
|
||||
"&AnyProviderIdEquals=" + show_provider_ids
|
||||
@@ -198,21 +200,21 @@ def __get_parent_id_from(params):
|
||||
if len(show) == 1:
|
||||
result = content.get("Items")[0].get("Id")
|
||||
else:
|
||||
log.debug("TV show not found for ids: {}", show_provider_ids)
|
||||
log.debug("TV show not found for ids: {}".format(show_provider_ids))
|
||||
else:
|
||||
log.error("TV show parameter not found in request.")
|
||||
return result
|
||||
|
||||
|
||||
def toggle_watched(params):
|
||||
log.debug("toggle_watched: {0}", params)
|
||||
log.debug("toggle_watched: {0}".format(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)
|
||||
log.debug("toggle_watched item info: {0}", result)
|
||||
log.debug("toggle_watched item info: {0}".format(result))
|
||||
|
||||
user_data = result.get("UserData", None)
|
||||
if user_data is None:
|
||||
@@ -225,35 +227,35 @@ def toggle_watched(params):
|
||||
|
||||
|
||||
def mark_item_watched(item_id):
|
||||
log.debug("Mark Item Watched: {0}", 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")
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
if last_url:
|
||||
log.debug("markWatched_lastUrl: {0}", last_url)
|
||||
log.debug("markWatched_lastUrl: {0}".format(last_url))
|
||||
home_window.set_property("skip_cache_for_" + last_url, "true")
|
||||
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
|
||||
def mark_item_unwatched(item_id):
|
||||
log.debug("Mark Item UnWatched: {0}", item_id)
|
||||
log.debug("Mark Item UnWatched: {0}".format(item_id))
|
||||
url = "{server}/Users/{userid}/PlayedItems/" + item_id
|
||||
downloadUtils.download_url(url, method="DELETE")
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
if last_url:
|
||||
log.debug("markUnwatched_lastUrl: {0}", last_url)
|
||||
log.debug("markUnwatched_lastUrl: {0}".format(last_url))
|
||||
home_window.set_property("skip_cache_for_" + last_url, "true")
|
||||
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
|
||||
def mark_item_favorite(item_id):
|
||||
log.debug("Add item to favourites: {0}", 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")
|
||||
check_for_new_content()
|
||||
@@ -266,7 +268,7 @@ def mark_item_favorite(item_id):
|
||||
|
||||
|
||||
def unmark_item_favorite(item_id):
|
||||
log.debug("Remove item from favourites: {0}", 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")
|
||||
check_for_new_content()
|
||||
@@ -280,8 +282,7 @@ def unmark_item_favorite(item_id):
|
||||
|
||||
def delete(item_id):
|
||||
|
||||
json_data = downloadUtils.download_url("{server}/Users/{userid}/Items/" + item_id + "?format=json")
|
||||
item = json.loads(json_data)
|
||||
item = downloadUtils.download_url("{server}/Users/{userid}/Items/" + item_id + "?format=json")
|
||||
|
||||
item_id = item.get("Id")
|
||||
item_name = item.get("Name", "")
|
||||
@@ -302,9 +303,9 @@ def delete(item_id):
|
||||
xbmcgui.Dialog().ok(string_load(30135), string_load(30417), final_name)
|
||||
return
|
||||
|
||||
return_value = xbmcgui.Dialog().yesno(string_load(30091), final_name, string_load(30092))
|
||||
return_value = xbmcgui.Dialog().yesno(string_load(30091), '{}\n{}'.format(final_name, string_load(30092)))
|
||||
if return_value:
|
||||
log.debug('Deleting Item: {0}', item_id)
|
||||
log.debug('Deleting Item: {0}'.format(item_id))
|
||||
url = '{server}/Items/' + item_id
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(string_load(30052), string_load(30053))
|
||||
@@ -320,40 +321,28 @@ def delete(item_id):
|
||||
|
||||
|
||||
def get_params():
|
||||
'''
|
||||
Retrieve the request data from Kodi
|
||||
'''
|
||||
|
||||
plugin_path = sys.argv[0]
|
||||
paramstring = sys.argv[2]
|
||||
|
||||
log.debug("Parameter string: {0}", paramstring)
|
||||
log.debug("Plugin Path string: {0}", plugin_path)
|
||||
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}", param)
|
||||
log.debug("JellyCon -> Detected parameters: {0}".format(param))
|
||||
return param
|
||||
|
||||
|
||||
def show_menu(params):
|
||||
log.debug("showMenu(): {0}", params)
|
||||
log.debug("showMenu(): {0}".format(params))
|
||||
|
||||
home_window = HomeWindow()
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -362,7 +351,7 @@ def show_menu(params):
|
||||
url = "{server}/Users/{userid}/Items/" + item_id + "?format=json"
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(url)
|
||||
log.debug("Menu item info: {0}", result)
|
||||
log.debug("Menu item info: {0}".format(result))
|
||||
|
||||
if result is None:
|
||||
return
|
||||
@@ -466,20 +455,18 @@ def show_menu(params):
|
||||
view_key = "view-" + container_content_type
|
||||
current_default_view = settings.getSetting(view_key)
|
||||
view_match = container_view_id == current_default_view
|
||||
log.debug("View ID:{0} Content type:{1}", container_view_id, container_content_type)
|
||||
log.debug("View ID:{0} Content type:{1}".format(container_view_id, container_content_type))
|
||||
|
||||
if container_content_type in ["movies", "tvshows", "seasons", "episodes", "sets"]:
|
||||
if view_match:
|
||||
li = xbmcgui.ListItem("Unset as defalt view")
|
||||
li = xbmcgui.ListItem("Unset as default view")
|
||||
li.setProperty('menu_id', 'unset_view')
|
||||
action_items.append(li)
|
||||
else:
|
||||
li = xbmcgui.ListItem("Set as defalt view")
|
||||
li = xbmcgui.ListItem("Set as default view")
|
||||
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()
|
||||
@@ -487,22 +474,19 @@ def show_menu(params):
|
||||
selected_action = ""
|
||||
if selected_action_item is not None:
|
||||
selected_action = selected_action_item.getProperty('menu_id')
|
||||
log.debug("Menu Action Selected: {0}", selected_action)
|
||||
log.debug("Menu Action Selected: {0}".format(selected_action))
|
||||
del action_menu
|
||||
|
||||
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":
|
||||
log.debug("Settign view type for {0} to {1}", view_key, container_view_id)
|
||||
log.debug("Settign view type for {0} to {1}".format(view_key, container_view_id))
|
||||
settings.setSetting(view_key, container_view_id)
|
||||
|
||||
elif selected_action == "unset_view":
|
||||
log.debug("Un-Settign view type for {0} to {1}", view_key, container_view_id)
|
||||
log.debug("Un-Settign view type for {0} to {1}".format(view_key, container_view_id))
|
||||
settings.setSetting(view_key, "")
|
||||
|
||||
elif selected_action == "refresh_server":
|
||||
@@ -513,7 +497,7 @@ def show_menu(params):
|
||||
"&ReplaceAllImages=true" +
|
||||
"&ReplaceAllMetadata=true")
|
||||
res = downloadUtils.download_url(url, post_body="", method="POST")
|
||||
log.debug("Refresh Server Responce: {0}", res)
|
||||
log.debug("Refresh Server Responce: {0}".format(res))
|
||||
|
||||
elif selected_action == "hide":
|
||||
user_details = load_user_details(settings)
|
||||
@@ -522,13 +506,13 @@ def show_menu(params):
|
||||
url = "{server}/Items/" + item_id + "/Tags/Add"
|
||||
post_tag_data = {"Tags": [{"Name": hide_tag_string}]}
|
||||
res = downloadUtils.download_url(url, post_body=post_tag_data, method="POST")
|
||||
log.debug("Add Tag Responce: {0}", res)
|
||||
log.debug("Add Tag Responce: {0}".format(res))
|
||||
|
||||
check_for_new_content()
|
||||
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
if last_url:
|
||||
log.debug("markUnwatched_lastUrl: {0}", last_url)
|
||||
log.debug("markUnwatched_lastUrl: {0}".format(last_url))
|
||||
home_window.set_property("skip_cache_for_" + last_url, "true")
|
||||
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
@@ -549,7 +533,7 @@ def show_menu(params):
|
||||
bitrate_dialog.doModal()
|
||||
selected_transcode_value = bitrate_dialog.selected_transcode_value
|
||||
del bitrate_dialog
|
||||
log.debug("selected_transcode_value: {0}", selected_transcode_value)
|
||||
log.debug("selected_transcode_value: {0}".format(selected_transcode_value))
|
||||
|
||||
if selected_transcode_value > 0:
|
||||
settings.setSetting("force_max_stream_bitrate", str(selected_transcode_value))
|
||||
@@ -577,11 +561,10 @@ def show_menu(params):
|
||||
|
||||
elif selected_action == "safe_delete":
|
||||
url = "{server}/jellyfin_safe_delete/delete_item/" + item_id
|
||||
delete_action = downloadUtils.download_url(url)
|
||||
result = json.loads(delete_action)
|
||||
result = downloadUtils.download_url(url)
|
||||
dialog = xbmcgui.Dialog()
|
||||
if result:
|
||||
log.debug("Safe_Delete_Action: {0}", 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]"
|
||||
@@ -611,7 +594,7 @@ def show_menu(params):
|
||||
confirm_dialog.message = message
|
||||
confirm_dialog.heading = "Confirm delete files?"
|
||||
confirm_dialog.doModal()
|
||||
log.debug("safe_delete_confirm_dialog: {0}", confirm_dialog.confirm)
|
||||
log.debug("safe_delete_confirm_dialog: {0}".format(confirm_dialog.confirm))
|
||||
|
||||
if confirm_dialog.confirm:
|
||||
url = "{server}/jellyfin_safe_delete/delete_item_action"
|
||||
@@ -620,12 +603,11 @@ def show_menu(params):
|
||||
'action_token': action_token
|
||||
}
|
||||
delete_action = downloadUtils.download_url(url, method="POST", post_body=playback_info)
|
||||
log.debug("Delete result action: {0}", delete_action)
|
||||
delete_action_result = json.loads(delete_action)
|
||||
if not delete_action_result:
|
||||
dialog.ok("Error", "Error deleteing files", "Error in responce from server")
|
||||
elif not delete_action_result["result"]:
|
||||
dialog.ok("Error", "Error deleteing files", delete_action_result["message"])
|
||||
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:
|
||||
@@ -634,7 +616,7 @@ def show_menu(params):
|
||||
elif selected_action == "show_extras":
|
||||
# "http://localhost:8096/Users/3138bed521e5465b9be26d2c63be94af/Items/78/SpecialFeatures"
|
||||
u = "{server}/Users/{userid}/Items/" + item_id + "/SpecialFeatures"
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Videos")
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Videos")
|
||||
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
|
||||
xbmc.executebuiltin(built_in_command)
|
||||
|
||||
@@ -650,7 +632,7 @@ def show_menu(params):
|
||||
'&IsMissing=false' +
|
||||
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
|
||||
'&format=json')
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Season")
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Season")
|
||||
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
|
||||
xbmc.executebuiltin(built_in_command)
|
||||
|
||||
@@ -667,12 +649,11 @@ def show_menu(params):
|
||||
'&Fields={field_filters}' +
|
||||
'&format=json')
|
||||
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Series")
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Series")
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(home)"):
|
||||
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
|
||||
else:
|
||||
# built_in_command = 'Container.Update(' + action_url + ', replace)'
|
||||
built_in_command = 'Container.Update(' + action_url + ')'
|
||||
|
||||
xbmc.executebuiltin(built_in_command)
|
||||
@@ -686,29 +667,11 @@ def show_menu(params):
|
||||
|
||||
|
||||
def populate_listitem(item_id):
|
||||
log.debug("populate_listitem: {0}", item_id)
|
||||
log.debug("populate_listitem: {0}".format(item_id))
|
||||
|
||||
url = "{server}/Users/{userid}/Items/" + item_id + "?format=json"
|
||||
json_data = downloadUtils.download_url(url)
|
||||
result = json.loads(json_data)
|
||||
log.debug("populate_listitem item info: {0}", result)
|
||||
|
||||
'''
|
||||
server = downloadUtils.getServer()
|
||||
gui_options = {}
|
||||
gui_options["server"] = server
|
||||
|
||||
gui_options["name_format"] = None
|
||||
gui_options["name_format_type"] = None
|
||||
|
||||
details, extraData = extract_item_info(result,gui_options )
|
||||
u, list_item, folder = add_gui_item(result["Id"], details, extraData, {}, folder=False)
|
||||
|
||||
log.debug("list_item path: {0}", u)
|
||||
|
||||
#list_item.setProperty('IsPlayable', 'false')
|
||||
#list_item.setPath(u)
|
||||
'''
|
||||
result = downloadUtils.download_url(url)
|
||||
log.debug("populate_listitem item info: {0}".format(result))
|
||||
|
||||
item_title = result.get("Name", string_load(30280))
|
||||
|
||||
@@ -738,7 +701,7 @@ def populate_listitem(item_id):
|
||||
|
||||
|
||||
def show_content(params):
|
||||
log.debug("showContent Called: {0}", params)
|
||||
log.debug("showContent Called: {0}".format(params))
|
||||
|
||||
item_type = params.get("item_type")
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -760,7 +723,7 @@ def show_content(params):
|
||||
"&IsVirtualUnaired=false" +
|
||||
"&IncludeItemTypes=" + item_type)
|
||||
|
||||
log.debug("showContent Content Url: {0}", content_url)
|
||||
log.debug("showContent Content Url: {0}".format(content_url))
|
||||
get_content(content_url, params)
|
||||
|
||||
|
||||
@@ -771,33 +734,20 @@ def search_results_person(params):
|
||||
person_id = params.get("person_id")
|
||||
details_url = ('{server}/Users/{userid}/items' +
|
||||
'?PersonIds=' + person_id +
|
||||
# '&IncludeItemTypes=Movie' +
|
||||
'&Recursive=true' +
|
||||
'&Fields={field_filters}' +
|
||||
'&format=json')
|
||||
|
||||
'''
|
||||
details_result = dataManager.GetContent(details_url)
|
||||
log.debug("Search Results Details: {0}", details_result)
|
||||
|
||||
if details_result:
|
||||
items = details_result.get("Items")
|
||||
found_types = set()
|
||||
for item in items:
|
||||
found_types.add(item.get("Type"))
|
||||
log.debug("search_results_person found_types: {0}", found_types)
|
||||
'''
|
||||
|
||||
params["name_format"] = "Episode|episode_name_format"
|
||||
|
||||
dir_items, detected_type, total_records = process_directory(details_url, None, params)
|
||||
|
||||
log.debug('search_results_person results: {0}', dir_items)
|
||||
log.debug('search_results_person detect_type: {0}', detected_type)
|
||||
log.debug('search_results_person results: {0}'.format(dir_items))
|
||||
log.debug('search_results_person detect_type: {0}'.format(detected_type))
|
||||
|
||||
if detected_type is not None:
|
||||
# if the media type is not set then try to use the detected type
|
||||
log.debug("Detected content type: {0}", detected_type)
|
||||
log.debug("Detected content type: {0}".format(detected_type))
|
||||
content_type = None
|
||||
|
||||
if detected_type == "Movie":
|
||||
@@ -812,8 +762,6 @@ def search_results_person(params):
|
||||
if content_type:
|
||||
xbmcplugin.setContent(handle, content_type)
|
||||
|
||||
# xbmcplugin.setContent(handle, detected_type)
|
||||
|
||||
if dir_items is not None:
|
||||
xbmcplugin.addDirectoryItems(handle, dir_items)
|
||||
|
||||
@@ -825,9 +773,9 @@ def search_results(params):
|
||||
item_type = params.get('item_type')
|
||||
query_string = params.get('query')
|
||||
if query_string:
|
||||
log.debug("query_string : {0}", query_string)
|
||||
query_string = urllib.unquote(query_string)
|
||||
log.debug("query_string : {0}", query_string)
|
||||
log.debug("query_string : {0}".format(query_string))
|
||||
query_string = unquote(query_string)
|
||||
log.debug("query_string : {0}".format(query_string))
|
||||
|
||||
item_type = item_type.lower()
|
||||
|
||||
@@ -867,14 +815,14 @@ def search_results(params):
|
||||
return
|
||||
|
||||
home_window.set_property("last_search", user_input)
|
||||
log.debug('searchResults Called: {0}', params)
|
||||
log.debug('searchResults Called: {0}'.format(params))
|
||||
query = user_input
|
||||
|
||||
else:
|
||||
query = query_string
|
||||
|
||||
query = urllib.quote(query)
|
||||
log.debug("query : {0}", query)
|
||||
query = quote(query)
|
||||
log.debug("query : {0}".format(query))
|
||||
|
||||
if (not item_type) or (not query):
|
||||
return
|
||||
@@ -904,7 +852,7 @@ def search_results(params):
|
||||
"&userId={userid}")
|
||||
|
||||
person_search_results = dataManager.get_content(search_url)
|
||||
log.debug("Person Search Result : {0}", person_search_results)
|
||||
log.debug("Person Search Result : {0}".format(person_search_results))
|
||||
if person_search_results is None:
|
||||
return
|
||||
|
||||
@@ -915,8 +863,6 @@ def search_results(params):
|
||||
for item in person_items:
|
||||
person_id = item.get('Id')
|
||||
person_name = item.get('Name')
|
||||
# image_tags = item.get('ImageTags', {})
|
||||
# image_tag = image_tags.get('PrimaryImageTag', '')
|
||||
person_thumbnail = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
|
||||
action_url = sys.argv[0] + "?mode=NEW_SEARCH_PERSON&person_id=" + person_id
|
||||
@@ -967,23 +913,30 @@ def search_results(params):
|
||||
def play_action(params):
|
||||
log.debug("== ENTER: PLAY ==")
|
||||
|
||||
log.debug("PLAY ACTION PARAMS: {0}", params)
|
||||
log.debug("PLAY ACTION PARAMS: {0}".format(params))
|
||||
item_id = params.get("item_id")
|
||||
|
||||
auto_resume = int(params.get("auto_resume", "-1"))
|
||||
log.debug("AUTO_RESUME: {0}", auto_resume)
|
||||
auto_resume = params.get("auto_resume", "-1")
|
||||
if auto_resume == 'None':
|
||||
auto_resume = '-1'
|
||||
if auto_resume:
|
||||
auto_resume = int(auto_resume)
|
||||
else:
|
||||
auto_resume = -1
|
||||
|
||||
log.debug("AUTO_RESUME: {0}".format(auto_resume))
|
||||
|
||||
force_transcode = params.get("force_transcode", None) is not None
|
||||
log.debug("FORCE_TRANSCODE: {0}", force_transcode)
|
||||
log.debug("FORCE_TRANSCODE: {0}".format(force_transcode))
|
||||
|
||||
media_source_id = params.get("media_source_id", "")
|
||||
log.debug("media_source_id: {0}", media_source_id)
|
||||
log.debug("media_source_id: {0}".format(media_source_id))
|
||||
|
||||
subtitle_stream_index = params.get("subtitle_stream_index")
|
||||
log.debug("subtitle_stream_index: {0}", subtitle_stream_index)
|
||||
log.debug("subtitle_stream_index: {0}".format(subtitle_stream_index))
|
||||
|
||||
audio_stream_index = params.get("audio_stream_index")
|
||||
log.debug("audio_stream_index: {0}", audio_stream_index)
|
||||
log.debug("audio_stream_index: {0}".format(audio_stream_index))
|
||||
|
||||
action = params.get("action", "play")
|
||||
|
||||
@@ -1001,8 +954,8 @@ def play_action(params):
|
||||
play_info["media_source_id"] = media_source_id
|
||||
play_info["subtitle_stream_index"] = subtitle_stream_index
|
||||
play_info["audio_stream_index"] = audio_stream_index
|
||||
log.info("Sending jellycon_play_action : {0}", play_info)
|
||||
send_event_notification("jellycon_play_action", play_info)
|
||||
log.info("Sending jellycon_play_action : {0}".format(play_info))
|
||||
play_file(play_info)
|
||||
|
||||
|
||||
def play_item_trailer(item_id):
|
||||
@@ -1010,13 +963,12 @@ def play_item_trailer(item_id):
|
||||
|
||||
url = ("{server}/Users/{userid}/Items/%s/LocalTrailers?format=json" % item_id)
|
||||
|
||||
json_data = downloadUtils.download_url(url)
|
||||
result = json.loads(json_data)
|
||||
result = downloadUtils.download_url(url)
|
||||
|
||||
if result is None:
|
||||
return
|
||||
|
||||
log.debug("LocalTrailers {0}", result)
|
||||
log.debug("LocalTrailers {0}".format(result))
|
||||
count = 1
|
||||
|
||||
trailer_names = []
|
||||
@@ -1035,9 +987,8 @@ def play_item_trailer(item_id):
|
||||
trailer_list.append(info)
|
||||
|
||||
url = ("{server}/Users/{userid}/Items/%s?format=json&Fields=RemoteTrailers" % item_id)
|
||||
json_data = downloadUtils.download_url(url)
|
||||
result = json.loads(json_data)
|
||||
log.debug("RemoteTrailers: {0}", result)
|
||||
result = downloadUtils.download_url(url)
|
||||
log.debug("RemoteTrailers: {0}".format(result))
|
||||
count = 1
|
||||
|
||||
if result is None:
|
||||
@@ -1058,7 +1009,7 @@ def play_item_trailer(item_id):
|
||||
trailer_names.append(name)
|
||||
trailer_list.append(info)
|
||||
|
||||
log.debug("TrailerList: {0}", trailer_list)
|
||||
log.debug("TrailerList: {0}".format(trailer_list))
|
||||
|
||||
trailer_text = []
|
||||
for trailer in trailer_list:
|
||||
@@ -1069,7 +1020,7 @@ def play_item_trailer(item_id):
|
||||
resp = dialog.select(string_load(30308), trailer_text)
|
||||
if resp > -1:
|
||||
trailer = trailer_list[resp]
|
||||
log.debug("SelectedTrailer: {0}", trailer)
|
||||
log.debug("SelectedTrailer: {0}".format(trailer))
|
||||
|
||||
if trailer.get("type") == "local":
|
||||
params = {}
|
||||
@@ -1079,10 +1030,6 @@ def play_item_trailer(item_id):
|
||||
elif trailer.get("type") == "remote":
|
||||
youtube_id = trailer.get("url").rsplit('=', 1)[1]
|
||||
youtube_plugin = "RunPlugin(plugin://plugin.video.youtube/play/?video_id=%s)" % youtube_id
|
||||
log.debug("youtube_plugin: {0}", youtube_plugin)
|
||||
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)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcvfs
|
||||
import xbmc
|
||||
import base64
|
||||
import re
|
||||
from urlparse import urlparse
|
||||
from random import shuffle
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from six import ensure_text
|
||||
|
||||
import threading
|
||||
import httplib
|
||||
import requests
|
||||
import io
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .datamanager import DataManager
|
||||
from .downloadutils import DownloadUtils
|
||||
from .utils import get_art
|
||||
@@ -24,7 +26,7 @@ except Exception as err:
|
||||
pil_loaded = False
|
||||
|
||||
PORT_NUMBER = 24276
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def get_image_links(url):
|
||||
@@ -34,16 +36,6 @@ def get_image_links(url):
|
||||
if server is None:
|
||||
return []
|
||||
|
||||
# url = re.sub("(?i)limit=[0-9]+", "limit=4", url)
|
||||
# url = url.replace("{ItemLimit}", "4")
|
||||
# url = re.sub("(?i)SortBy=[a-zA-Z]+", "SortBy=Random", url)
|
||||
|
||||
# if not re.search('limit=', url, re.IGNORECASE):
|
||||
# url += "&Limit=4"
|
||||
|
||||
# if not re.search('sortBy=', url, re.IGNORECASE):
|
||||
# url += "&SortBy=Random"
|
||||
|
||||
url = re.sub("(?i)EnableUserData=[a-z]+", "EnableUserData=False", url)
|
||||
url = re.sub("(?i)EnableImageTypes=[,a-z]+", "EnableImageTypes=Primary", url)
|
||||
url = url.replace("{field_filters}", "BasicSyncInfo")
|
||||
@@ -78,15 +70,15 @@ def get_image_links(url):
|
||||
def build_image(path):
|
||||
log.debug("build_image()")
|
||||
|
||||
log.debug("Request Path : {0}", path)
|
||||
log.debug("Request Path : {0}".format(path))
|
||||
|
||||
request_path = path[1:]
|
||||
|
||||
if request_path == "favicon.ico":
|
||||
return []
|
||||
|
||||
decoded_url = base64.b64decode(request_path)
|
||||
log.debug("decoded_url : {0}", decoded_url)
|
||||
decoded_url = ensure_text(base64.b64decode(request_path))
|
||||
log.debug("decoded_url : {0}".format(decoded_url))
|
||||
|
||||
image_urls = get_image_links(decoded_url)
|
||||
|
||||
@@ -108,21 +100,17 @@ def build_image(path):
|
||||
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
# user_name = url_bits.username
|
||||
# user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_query = url_bits.query
|
||||
|
||||
server = "%s:%s" % (host_name, port)
|
||||
url_full_path = url_path + "?" + url_query
|
||||
|
||||
log.debug("Loading image from : {0} {1} {2}", image_count, server, url_full_path)
|
||||
log.debug("Loading image from : {0} {1} {2}".format(image_count, server, url_full_path))
|
||||
|
||||
try:
|
||||
conn = httplib.HTTPConnection(server)
|
||||
conn.request("GET", url_full_path)
|
||||
image_responce = conn.getresponse()
|
||||
image_data = image_responce.read()
|
||||
image_response = requests.get(thumb_url)
|
||||
image_data = image_response.content
|
||||
|
||||
loaded_image = Image.open(io.BytesIO(image_data))
|
||||
image = ImageOps.fit(loaded_image, size, method=Image.ANTIALIAS, bleed=0.0, centering=(0.5, 0.5))
|
||||
@@ -136,7 +124,7 @@ def build_image(path):
|
||||
del image_data
|
||||
|
||||
except Exception as con_err:
|
||||
log.debug("Error loading image : {0}", str(con_err))
|
||||
log.debug("Error loading image : {0}".format(con_err))
|
||||
|
||||
image_count += 1
|
||||
|
||||
@@ -170,12 +158,6 @@ class HttpImageHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
def do_QUIT(self):
|
||||
log.debug("HttpImageHandler:do_QUIT()")
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
def serve_image(self):
|
||||
|
||||
if pil_loaded:
|
||||
@@ -205,26 +187,22 @@ class HttpImageHandler(BaseHTTPRequestHandler):
|
||||
|
||||
class HttpImageServerThread(threading.Thread):
|
||||
|
||||
keep_running = True
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
self.keep_running = True
|
||||
|
||||
def stop(self):
|
||||
self.keep_running = False
|
||||
log.debug("HttpImageServerThread:stop called")
|
||||
try:
|
||||
conn = httplib.HTTPConnection("localhost:%d" % PORT_NUMBER)
|
||||
conn.request("QUIT", "/")
|
||||
conn.getresponse()
|
||||
except:
|
||||
pass
|
||||
self.keep_running = False
|
||||
self.server.shutdown()
|
||||
|
||||
def run(self):
|
||||
log.debug("HttpImageServerThread:started")
|
||||
server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
self.server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
|
||||
while self.keep_running:
|
||||
server.handle_request()
|
||||
self.server.serve_forever()
|
||||
xbmc.sleep(1000)
|
||||
|
||||
log.debug("HttpImageServerThread:exiting")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
import os
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -12,11 +13,12 @@ import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from .utils import get_art, datetime_from_string
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .downloadutils import DownloadUtils
|
||||
from .kodi_utils import HomeWindow
|
||||
from six import ensure_text
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
|
||||
addon_instance = xbmcaddon.Addon()
|
||||
@@ -100,27 +102,27 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
item_details = ItemDetails()
|
||||
|
||||
item_details.id = item["Id"]
|
||||
item_details.etag = item["Etag"]
|
||||
item_details.is_folder = item["IsFolder"]
|
||||
item_details.item_type = item["Type"]
|
||||
item_details.location_type = item["LocationType"]
|
||||
item_details.name = item["Name"]
|
||||
item_details.sort_name = item["SortName"]
|
||||
item_details.id = item.get("Id")
|
||||
item_details.etag = item.get("Etag")
|
||||
item_details.is_folder = item.get("IsFolder")
|
||||
item_details.item_type = item.get("Type")
|
||||
item_details.location_type = item.get("LocationType")
|
||||
item_details.name = item.get("Name")
|
||||
item_details.sort_name = item.get("SortName")
|
||||
item_details.original_title = item_details.name
|
||||
|
||||
if item_details.item_type == "Episode":
|
||||
item_details.episode_number = item["IndexNumber"]
|
||||
item_details.season_number = item["ParentIndexNumber"]
|
||||
item_details.series_id = item["SeriesId"]
|
||||
item_details.episode_number = item.get("IndexNumber")
|
||||
item_details.season_number = item.get("ParentIndexNumber")
|
||||
item_details.series_id = item.get("SeriesId")
|
||||
|
||||
if item_details.season_number != 0:
|
||||
item_details.season_sort_number = item_details.season_number
|
||||
item_details.episode_sort_number = item_details.episode_number
|
||||
else:
|
||||
special_after_season = item["AirsAfterSeasonNumber"]
|
||||
special_before_season = item["AirsBeforeSeasonNumber"]
|
||||
special_before_episode = item["AirsBeforeEpisodeNumber"]
|
||||
special_after_season = item.get("AirsAfterSeasonNumber")
|
||||
special_before_season = item.get("AirsBeforeSeasonNumber")
|
||||
special_before_episode = item.get("AirsBeforeEpisodeNumber")
|
||||
|
||||
if special_after_season:
|
||||
item_details.season_sort_number = special_after_season + 1
|
||||
@@ -131,21 +133,21 @@ def extract_item_info(item, gui_options):
|
||||
item_details.episode_sort_number = special_before_episode - 1
|
||||
|
||||
elif item_details.item_type == "Season":
|
||||
item_details.season_number = item["IndexNumber"]
|
||||
item_details.series_id = item["SeriesId"]
|
||||
item_details.season_number = item.get("IndexNumber")
|
||||
item_details.series_id = item.get("SeriesId")
|
||||
|
||||
elif item_details.item_type == "Series":
|
||||
item_details.status = item["Status"]
|
||||
item_details.status = item.get("Status")
|
||||
|
||||
elif item_details.item_type == "Audio":
|
||||
item_details.track_number = item["IndexNumber"]
|
||||
item_details.album_name = item["Album"]
|
||||
artists = item["Artists"]
|
||||
if artists is not None and len(artists) > 0:
|
||||
item_details.track_number = item.get("IndexNumber")
|
||||
item_details.album_name = item.get("Album")
|
||||
artists = item.get("Artists", [])
|
||||
if artists:
|
||||
item_details.song_artist = artists[0] # get first artist
|
||||
|
||||
elif item_details.item_type == "MusicAlbum":
|
||||
item_details.album_artist = item["AlbumArtist"]
|
||||
item_details.album_artist = item.get("AlbumArtist")
|
||||
item_details.album_name = item_details.name
|
||||
|
||||
if item_details.season_number is None:
|
||||
@@ -153,34 +155,34 @@ def extract_item_info(item, gui_options):
|
||||
if item_details.episode_number is None:
|
||||
item_details.episode_number = 0
|
||||
|
||||
if item["Taglines"] is not None and len(item["Taglines"]) > 0:
|
||||
item_details.tagline = item["Taglines"][0]
|
||||
if item.get("Taglines", []):
|
||||
item_details.tagline = item.get("Taglines")[0]
|
||||
|
||||
item_details.tags = []
|
||||
if item["TagItems"] is not None and len(item["TagItems"]) > 0:
|
||||
for tag_info in item["TagItems"]:
|
||||
item_details.tags.append(tag_info["Name"])
|
||||
if item.get("TagItems", []):
|
||||
for tag_info in item.get("TagItems"):
|
||||
item_details.tags.append(tag_info.get("Name"))
|
||||
|
||||
# set the item name
|
||||
# override with name format string from request
|
||||
name_format = gui_options["name_format"]
|
||||
name_format_type = gui_options["name_format_type"]
|
||||
name_format = gui_options.get("name_format")
|
||||
name_format_type = gui_options.get("name_format_type")
|
||||
|
||||
if name_format is not None and item_details.item_type == name_format_type:
|
||||
name_info = {}
|
||||
name_info["ItemName"] = item["Name"]
|
||||
season_name = item["SeriesName"]
|
||||
name_info["ItemName"] = item.get("Name")
|
||||
season_name = item.get("SeriesName")
|
||||
if season_name:
|
||||
name_info["SeriesName"] = season_name
|
||||
else:
|
||||
name_info["SeriesName"] = ""
|
||||
name_info["SeasonIndex"] = u"%02d" % item_details.season_number
|
||||
name_info["EpisodeIndex"] = u"%02d" % item_details.episode_number
|
||||
log.debug("FormatName: {0} | {1}", name_format, name_info)
|
||||
item_details.name = unicode(name_format).format(**name_info).strip()
|
||||
log.debug("FormatName: {0} | {1}".format(name_format, name_info))
|
||||
item_details.name = ensure_text(name_format).format(**name_info).strip()
|
||||
|
||||
year = item["ProductionYear"]
|
||||
prem_date = item["PremiereDate"]
|
||||
year = item.get("ProductionYear")
|
||||
prem_date = item.get("PremiereDate")
|
||||
|
||||
if year is not None:
|
||||
item_details.year = year
|
||||
@@ -191,35 +193,35 @@ def extract_item_info(item, gui_options):
|
||||
tokens = prem_date.split("T")
|
||||
item_details.premiere_date = tokens[0]
|
||||
|
||||
create_date = item["DateCreated"]
|
||||
if create_date is not None:
|
||||
create_date = item.get("DateCreated")
|
||||
if create_date:
|
||||
item_details.date_added = create_date.split('.')[0].replace('T', " ")
|
||||
|
||||
# add the premiered date for Upcoming TV
|
||||
if item_details.location_type == "Virtual":
|
||||
airtime = item["AirTime"]
|
||||
airtime = item.get("AirTime")
|
||||
item_details.name = item_details.name + ' - ' + item_details.premiere_date + ' - ' + str(airtime)
|
||||
|
||||
if item_details.item_type == "Program":
|
||||
item_details.program_channel_name = item["ChannelName"]
|
||||
item_details.program_start_date = item["StartDate"]
|
||||
item_details.program_end_date = item["EndDate"]
|
||||
item_details.program_channel_name = item.get("ChannelName")
|
||||
item_details.program_start_date = item.get("StartDate")
|
||||
item_details.program_end_date = item.get("EndDate")
|
||||
|
||||
# Process MediaStreams
|
||||
media_streams = item["MediaStreams"]
|
||||
if media_streams is not None:
|
||||
media_streams = item.get("MediaStreams", [])
|
||||
if media_streams:
|
||||
media_info_list = []
|
||||
for mediaStream in media_streams:
|
||||
stream_type = mediaStream["Type"]
|
||||
stream_type = mediaStream.get("Type")
|
||||
if stream_type == "Video":
|
||||
media_info = {}
|
||||
media_info["type"] = "video"
|
||||
media_info["codec"] = mediaStream["Codec"]
|
||||
media_info["height"] = mediaStream["Height"]
|
||||
media_info["width"] = mediaStream["Width"]
|
||||
aspect_ratio = mediaStream["AspectRatio"]
|
||||
media_info["codec"] = mediaStream.get("Codec")
|
||||
media_info["height"] = mediaStream.get("Height")
|
||||
media_info["width"] = mediaStream.get("Width")
|
||||
aspect_ratio = mediaStream.get("AspectRatio")
|
||||
media_info["apect"] = aspect_ratio
|
||||
if aspect_ratio is not None and len(aspect_ratio) >= 3:
|
||||
if aspect_ratio and len(aspect_ratio) >= 3:
|
||||
try:
|
||||
aspect_width, aspect_height = aspect_ratio.split(':')
|
||||
media_info["apect_ratio"] = float(aspect_width) / float(aspect_height)
|
||||
@@ -231,36 +233,35 @@ def extract_item_info(item, gui_options):
|
||||
if stream_type == "Audio":
|
||||
media_info = {}
|
||||
media_info["type"] = "audio"
|
||||
media_info["codec"] = mediaStream["Codec"]
|
||||
media_info["channels"] = mediaStream["Channels"]
|
||||
media_info["language"] = mediaStream["Language"]
|
||||
media_info["codec"] = mediaStream.get("Codec")
|
||||
media_info["channels"] = mediaStream.get("Channels")
|
||||
media_info["language"] = mediaStream.get("Language")
|
||||
media_info_list.append(media_info)
|
||||
if stream_type == "Subtitle":
|
||||
item_details.subtitle_available = True
|
||||
media_info = {}
|
||||
media_info["type"] = "sub"
|
||||
media_info["language"] = mediaStream["Language"]
|
||||
media_info["language"] = mediaStream.get("Language", '')
|
||||
media_info_list.append(media_info)
|
||||
|
||||
item_details.media_streams = media_info_list
|
||||
|
||||
# Process People
|
||||
people = item["People"]
|
||||
people = item.get("People", [])
|
||||
if people is not None:
|
||||
cast = []
|
||||
for person in people:
|
||||
person_type = person["Type"]
|
||||
person_type = person.get("Type")
|
||||
if person_type == "Director":
|
||||
item_details.director = item_details.director + person["Name"] + ' '
|
||||
item_details.director = item_details.director + person.get("Name") + ' '
|
||||
elif person_type == "Writing":
|
||||
item_details.writer = person["Name"]
|
||||
elif person_type == "Actor":
|
||||
# log.debug("Person: {0}", person)
|
||||
person_name = person["Name"]
|
||||
person_role = person["Role"]
|
||||
person_id = person["Id"]
|
||||
person_tag = person["PrimaryImageTag"]
|
||||
if person_tag is not None:
|
||||
person_name = person.get("Name")
|
||||
person_role = person.get("Role")
|
||||
person_id = person.get("Id")
|
||||
person_tag = person.get("PrimaryImageTag")
|
||||
if person_tag:
|
||||
person_thumbnail = download_utils.image_url(person_id,
|
||||
"Primary", 0, 400, 400,
|
||||
person_tag,
|
||||
@@ -272,64 +273,61 @@ def extract_item_info(item, gui_options):
|
||||
item_details.cast = cast
|
||||
|
||||
# Process Studios
|
||||
studios = item["Studios"]
|
||||
studios = item.get("Studios", [])
|
||||
if studios is not None:
|
||||
for studio in studios:
|
||||
if item_details.studio is None: # Just take the first one
|
||||
studio_name = studio["Name"]
|
||||
studio_name = studio.get("Name")
|
||||
item_details.studio = studio_name
|
||||
break
|
||||
|
||||
# production location
|
||||
prod_location = item["ProductionLocations"]
|
||||
# log.debug("ProductionLocations : {0}", prod_location)
|
||||
if prod_location and len(prod_location) > 0:
|
||||
prod_location = item.get("ProductionLocations", [])
|
||||
if prod_location:
|
||||
item_details.production_location = prod_location[0]
|
||||
|
||||
# Process Genres
|
||||
genres = item["Genres"]
|
||||
if genres is not None and len(genres) > 0:
|
||||
genres = item.get("Genres", [])
|
||||
if genres:
|
||||
item_details.genres = genres
|
||||
|
||||
# Process UserData
|
||||
user_data = item["UserData"]
|
||||
if user_data is None:
|
||||
user_data = defaultdict(lambda: None, {})
|
||||
user_data = item.get("UserData", {})
|
||||
|
||||
if user_data["Played"] is True:
|
||||
if user_data.get("Played"):
|
||||
item_details.overlay = "6"
|
||||
item_details.play_count = 1
|
||||
else:
|
||||
item_details.overlay = "7"
|
||||
item_details.play_count = 0
|
||||
|
||||
if user_data["IsFavorite"] is True:
|
||||
if user_data.get("IsFavorite"):
|
||||
item_details.overlay = "5"
|
||||
item_details.favorite = "true"
|
||||
else:
|
||||
item_details.favorite = "false"
|
||||
|
||||
reasonable_ticks = user_data["PlaybackPositionTicks"]
|
||||
if reasonable_ticks is not None:
|
||||
reasonable_ticks = user_data.get("PlaybackPositionTicks", 0)
|
||||
if reasonable_ticks:
|
||||
reasonable_ticks = int(reasonable_ticks) / 1000
|
||||
item_details.resume_time = int(reasonable_ticks / 10000)
|
||||
|
||||
item_details.series_name = item["SeriesName"]
|
||||
item_details.plot = item["Overview"]
|
||||
item_details.series_name = item.get("SeriesName", '')
|
||||
item_details.plot = item.get("Overview", '')
|
||||
|
||||
runtime = item["RunTimeTicks"]
|
||||
if item_details.is_folder is False and runtime is not None:
|
||||
item_details.duration = long(runtime) / 10000000
|
||||
runtime = item.get("RunTimeTicks")
|
||||
if item_details.is_folder is False and runtime:
|
||||
item_details.duration = runtime / 10000000
|
||||
|
||||
child_count = item["ChildCount"]
|
||||
if child_count is not None:
|
||||
child_count = item.get("ChildCount")
|
||||
if child_count:
|
||||
item_details.total_seasons = child_count
|
||||
|
||||
recursive_item_count = item["RecursiveItemCount"]
|
||||
if recursive_item_count is not None:
|
||||
recursive_item_count = item.get("RecursiveItemCount")
|
||||
if recursive_item_count:
|
||||
item_details.total_episodes = recursive_item_count
|
||||
|
||||
unplayed_item_count = user_data["UnplayedItemCount"]
|
||||
unplayed_item_count = user_data.get("UnplayedItemCount")
|
||||
if unplayed_item_count is not None:
|
||||
item_details.unwatched_episodes = unplayed_item_count
|
||||
item_details.watched_episodes = item_details.total_episodes - unplayed_item_count
|
||||
@@ -337,20 +335,20 @@ def extract_item_info(item, gui_options):
|
||||
item_details.number_episodes = item_details.total_episodes
|
||||
|
||||
item_details.art = get_art(item, gui_options["server"])
|
||||
item_details.rating = item["OfficialRating"]
|
||||
item_details.mpaa = item["OfficialRating"]
|
||||
item_details.rating = item.get("OfficialRating")
|
||||
item_details.mpaa = item.get("OfficialRating")
|
||||
|
||||
item_details.community_rating = item["CommunityRating"]
|
||||
if item_details.community_rating is None:
|
||||
item_details.community_rating = item.get("CommunityRating")
|
||||
if not item_details.community_rating:
|
||||
item_details.community_rating = 0.0
|
||||
|
||||
item_details.critic_rating = item["CriticRating"]
|
||||
if item_details.critic_rating is None:
|
||||
item_details.critic_rating = item.get("CriticRating")
|
||||
if not item_details.critic_rating:
|
||||
item_details.critic_rating = 0.0
|
||||
|
||||
item_details.location_type = item["LocationType"]
|
||||
item_details.recursive_item_count = item["RecursiveItemCount"]
|
||||
item_details.recursive_unplayed_items_count = user_data["UnplayedItemCount"]
|
||||
item_details.location_type = item.get("LocationType")
|
||||
item_details.recursive_item_count = item.get("RecursiveItemCount")
|
||||
item_details.recursive_unplayed_items_count = user_data.get("UnplayedItemCount")
|
||||
|
||||
item_details.mode = "GET_CONTENT"
|
||||
|
||||
@@ -359,8 +357,6 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
def add_gui_item(url, item_details, display_options, folder=True, default_sort=False):
|
||||
|
||||
# log.debug("item_details: {0}", item_details.__dict__)
|
||||
|
||||
if not item_details.name:
|
||||
return None
|
||||
|
||||
@@ -371,9 +367,9 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
|
||||
# Create the URL to pass to the item
|
||||
if folder:
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url) + mode + "&media_type=" + item_details.item_type
|
||||
u = sys.argv[0] + "?url=" + quote(url) + mode + "&media_type=" + item_details.item_type
|
||||
if item_details.name_format:
|
||||
u += '&name_format=' + urllib.quote(item_details.name_format)
|
||||
u += '&name_format=' + quote(item_details.name_format)
|
||||
if default_sort:
|
||||
u += '&sort=none'
|
||||
else:
|
||||
@@ -450,8 +446,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
else:
|
||||
list_item = xbmcgui.ListItem(list_item_name, iconImage=thumb_path, thumbnailImage=thumb_path)
|
||||
|
||||
# log.debug("Setting thumbnail as: {0}", thumbPath)
|
||||
|
||||
item_properties = {}
|
||||
|
||||
# calculate percentage
|
||||
@@ -497,11 +491,11 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["rating"] = item_details.rating
|
||||
info_labels["year"] = item_details.year
|
||||
|
||||
if item_details.genres is not None and len(item_details.genres) > 0:
|
||||
if item_details.genres:
|
||||
genres_list = []
|
||||
for genre in item_details.genres:
|
||||
genres_list.append(urllib.quote(genre.encode('utf8')))
|
||||
item_properties["genres"] = urllib.quote("|".join(genres_list))
|
||||
genres_list.append(quote(genre.encode('utf8')))
|
||||
item_properties["genres"] = quote("|".join(genres_list))
|
||||
|
||||
info_labels["genre"] = " / ".join(item_details.genres)
|
||||
|
||||
@@ -569,7 +563,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["trailer"] = "plugin://plugin.video.jellycon?mode=playTrailer&id=" + item_details.id
|
||||
|
||||
list_item.setInfo('video', info_labels)
|
||||
# log.debug("info_labels: {0}", info_labels)
|
||||
|
||||
if item_details.media_streams is not None:
|
||||
for stream in item_details.media_streams:
|
||||
@@ -596,7 +589,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
item_properties["NumEpisodes"] = str(item_details.number_episodes)
|
||||
|
||||
list_item.setRating("imdb", item_details.community_rating, 0, True)
|
||||
# list_item.setRating("rt", item_details.critic_rating, 0, False)
|
||||
item_properties["TotalTime"] = str(item_details.duration)
|
||||
|
||||
else:
|
||||
@@ -607,7 +599,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["artist"] = item_details.song_artist
|
||||
info_labels["album"] = item_details.album_name
|
||||
|
||||
# log.debug("info_labels: {0}", info_labels)
|
||||
list_item.setInfo('music', info_labels)
|
||||
|
||||
list_item.setContentLookup(False)
|
||||
@@ -617,7 +608,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
if item_details.baseline_itemname is not None:
|
||||
item_properties["suggested_from_watching"] = item_details.baseline_itemname
|
||||
|
||||
# log.debug("item_properties: {0}", item_properties)
|
||||
if kodi_version > 17:
|
||||
list_item.setProperties(item_properties)
|
||||
else:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import xbmc
|
||||
|
||||
@@ -9,7 +11,7 @@ class JsonRpc(object):
|
||||
params = None
|
||||
|
||||
def __init__(self, method, **kwargs):
|
||||
|
||||
|
||||
self.method = method
|
||||
|
||||
for arg in kwargs: # id_(int), jsonrpc(str)
|
||||
@@ -18,7 +20,7 @@ class JsonRpc(object):
|
||||
def _query(self):
|
||||
|
||||
query = {
|
||||
|
||||
|
||||
'jsonrpc': self.jsonrpc,
|
||||
'id': self.id_,
|
||||
'method': self.method,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
@@ -6,9 +8,9 @@ import xbmcaddon
|
||||
import sys
|
||||
import json
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
addon = xbmcaddon.Addon()
|
||||
|
||||
|
||||
@@ -24,17 +26,14 @@ class HomeWindow:
|
||||
def get_property(self, key):
|
||||
key = self.id_string % key
|
||||
value = self.window.getProperty(key)
|
||||
# log.debug('HomeWindow: getProperty |{0}| -> |{1}|', key, value)
|
||||
return value
|
||||
|
||||
def set_property(self, key, value):
|
||||
key = self.id_string % key
|
||||
# log.debug('HomeWindow: setProperty |{0}| -> |{1}|', key, value)
|
||||
self.window.setProperty(key, value)
|
||||
|
||||
def clear_property(self, key):
|
||||
key = self.id_string % key
|
||||
# log.debug('HomeWindow: clearProperty |{0}|', key)
|
||||
self.window.clearProperty(key)
|
||||
|
||||
|
||||
@@ -59,9 +58,9 @@ def get_kodi_version():
|
||||
result = result.get("result")
|
||||
version_data = result.get("version")
|
||||
version = float(str(version_data.get("major")) + "." + str(version_data.get("minor")))
|
||||
log.debug("Version: {0} - {1}", version, version_data)
|
||||
log.debug("Version: {0} - {1}".format(version, version_data))
|
||||
except:
|
||||
version = 0.0
|
||||
log.error("Version Error : RAW Version Data: {0}", result)
|
||||
log.error("Version Error : RAW Version Data: {0}".format(result))
|
||||
|
||||
return version
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import xbmc
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .widgets import check_for_new_content
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class LibraryChangeMonitor(threading.Thread):
|
||||
|
||||
147
resources/lib/loghandler.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from six import ensure_text
|
||||
from kodi_six import xbmc, xbmcaddon
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
##################################################################################################
|
||||
|
||||
__addon__ = xbmcaddon.Addon(id='plugin.video.jellycon')
|
||||
__pluginpath__ = xbmc.translatePath(__addon__.getAddonInfo('path'))
|
||||
|
||||
##################################################################################################
|
||||
|
||||
|
||||
def getLogger(name=None):
|
||||
if name is None:
|
||||
return __LOGGER
|
||||
|
||||
return __LOGGER.getChild(name)
|
||||
|
||||
|
||||
class LogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(MyFormatter())
|
||||
|
||||
self.sensitive = {'Token': [], 'Server': []}
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
self.server = settings.getSetting('server_address')
|
||||
self.debug = settings.getSetting('log_debug')
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
if self._get_log_level(record.levelno):
|
||||
string = self.format(record)
|
||||
|
||||
# Hide server URL in logs
|
||||
string = string.replace(self.server or "{server}", "{jellyfin-server}")
|
||||
|
||||
py_version = sys.version_info.major
|
||||
# Log level notation changed in Kodi v19
|
||||
if py_version > 2:
|
||||
log_level = xbmc.LOGINFO
|
||||
else:
|
||||
log_level = xbmc.LOGNOTICE
|
||||
xbmc.log(string, level=log_level)
|
||||
|
||||
def _get_log_level(self, level):
|
||||
|
||||
levels = {
|
||||
logging.ERROR: 0,
|
||||
logging.WARNING: 0,
|
||||
logging.INFO: 1,
|
||||
logging.DEBUG: 2
|
||||
}
|
||||
if self.debug == 'true':
|
||||
log_level = 2
|
||||
else:
|
||||
log_level = 1
|
||||
|
||||
return log_level >= levels[level]
|
||||
|
||||
|
||||
class MyFormatter(logging.Formatter):
|
||||
|
||||
def __init__(self, fmt='%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s'):
|
||||
logging.Formatter.__init__(self, fmt)
|
||||
|
||||
def format(self, record):
|
||||
if record.pathname:
|
||||
record.pathname = ensure_text(record.pathname, get_filesystem_encoding())
|
||||
|
||||
self._gen_rel_path(record)
|
||||
|
||||
# Call the original formatter class to do the grunt work
|
||||
result = logging.Formatter.format(self, record)
|
||||
|
||||
return result
|
||||
|
||||
def formatException(self, exc_info):
|
||||
_pluginpath_real = os.path.realpath(__pluginpath__)
|
||||
res = []
|
||||
|
||||
for o in traceback.format_exception(*exc_info):
|
||||
o = ensure_text(o, get_filesystem_encoding())
|
||||
|
||||
if o.startswith(' File "'):
|
||||
# If this split can't handle your file names, you should seriously consider renaming your files.
|
||||
fn = o.split(' File "', 2)[1].split('", line ', 1)[0]
|
||||
rfn = os.path.realpath(fn)
|
||||
if rfn.startswith(_pluginpath_real):
|
||||
o = o.replace(fn, os.path.relpath(rfn, _pluginpath_real))
|
||||
|
||||
res.append(o)
|
||||
|
||||
return ''.join(res)
|
||||
|
||||
def _gen_rel_path(self, record):
|
||||
if record.pathname:
|
||||
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
if not enc:
|
||||
enc = sys.getdefaultencoding()
|
||||
|
||||
if not enc or enc == 'ascii':
|
||||
enc = 'utf-8'
|
||||
|
||||
return enc
|
||||
|
||||
|
||||
__LOGGER = logging.getLogger('JELLYFIN')
|
||||
for handler in __LOGGER.handlers:
|
||||
__LOGGER.removeHandler(handler)
|
||||
|
||||
__LOGGER.addHandler(LogHandler())
|
||||
__LOGGER.setLevel(logging.DEBUG)
|
||||
@@ -1,29 +1,31 @@
|
||||
# coding=utf-8
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
import json
|
||||
import urllib
|
||||
from six import ensure_binary, ensure_text
|
||||
from six.moves.urllib.parse import quote
|
||||
import base64
|
||||
import string
|
||||
|
||||
import xbmcplugin
|
||||
import xbmcaddon
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .kodi_utils import add_menu_directory_item, HomeWindow
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .translation import string_load
|
||||
from .datamanager import DataManager
|
||||
from .utils import get_art, get_jellyfin_url
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
downloadUtils = DownloadUtils()
|
||||
|
||||
__addon__ = xbmcaddon.Addon()
|
||||
|
||||
|
||||
def show_movie_tags(menu_params):
|
||||
log.debug("show_movie_tags: {0}", menu_params)
|
||||
log.debug("show_movie_tags: {0}".format(menu_params))
|
||||
parent_id = menu_params.get("parent_id")
|
||||
|
||||
url_params = {}
|
||||
@@ -50,7 +52,7 @@ def show_movie_tags(menu_params):
|
||||
|
||||
tags = result.get("Items")
|
||||
|
||||
log.debug("Tags : {0}", result)
|
||||
log.debug("Tags : {0}".format(result))
|
||||
|
||||
for tag in tags:
|
||||
name = tag["Name"]
|
||||
@@ -73,21 +75,21 @@ def show_movie_tags(menu_params):
|
||||
|
||||
item_url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
|
||||
|
||||
content_url = urllib.quote(item_url)
|
||||
content_url = quote(item_url)
|
||||
url = sys.argv[0] + ("?url=" +
|
||||
content_url +
|
||||
"&mode=GET_CONTENT" +
|
||||
"&media_type=movies")
|
||||
log.debug("addMenuDirectoryItem: {0} - {1}", name, url)
|
||||
log.debug("addMenuDirectoryItem: {0} - {1}".format(name, url))
|
||||
add_menu_directory_item(name, url, art=art)
|
||||
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
|
||||
|
||||
def show_movie_years(menu_params):
|
||||
log.debug("show_movie_years: {0}", menu_params)
|
||||
log.debug("show_movie_years: {0}".format(menu_params))
|
||||
parent_id = menu_params.get("parent_id")
|
||||
group_into_decades = menu_params.get("group") == "true"
|
||||
|
||||
@@ -159,21 +161,21 @@ def show_movie_years(menu_params):
|
||||
|
||||
item_url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
|
||||
|
||||
content_url = urllib.quote(item_url)
|
||||
content_url = quote(item_url)
|
||||
url = sys.argv[0] + ("?url=" +
|
||||
content_url +
|
||||
"&mode=GET_CONTENT" +
|
||||
"&media_type=movies")
|
||||
log.debug("addMenuDirectoryItem: {0} - {1}", name, url)
|
||||
log.debug("addMenuDirectoryItem: {0} - {1}".format(name, url))
|
||||
add_menu_directory_item(name, url, art=art)
|
||||
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
|
||||
|
||||
def show_movie_pages(menu_params):
|
||||
log.debug("showMoviePages: {0}", menu_params)
|
||||
log.debug("showMoviePages: {0}".format(menu_params))
|
||||
|
||||
parent_id = menu_params.get("parent_id")
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -199,7 +201,7 @@ def show_movie_pages(menu_params):
|
||||
return
|
||||
|
||||
total_results = result.get("TotalRecordCount", 0)
|
||||
log.debug("showMoviePages TotalRecordCount {0}", total_results)
|
||||
log.debug("showMoviePages TotalRecordCount {0}".format(total_results))
|
||||
|
||||
if result == 0:
|
||||
return
|
||||
@@ -240,24 +242,24 @@ def show_movie_pages(menu_params):
|
||||
item_data['path'] = item_url
|
||||
item_data['media_type'] = 'movies'
|
||||
|
||||
item_data["art"] = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
|
||||
item_data['art'] = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
|
||||
|
||||
collections.append(item_data)
|
||||
start_index = start_index + page_limit
|
||||
|
||||
for collection in collections:
|
||||
content_url = urllib.quote(collection['path'])
|
||||
content_url = quote(collection['path'])
|
||||
url = sys.argv[0] + ("?url=" + content_url +
|
||||
"&mode=GET_CONTENT" +
|
||||
"&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} - {1} - {2}", collection.get('title'), url, collection.get("art"))
|
||||
log.debug("addMenuDirectoryItem: {0} - {1} - {2}".format(collection.get('title'), url, collection.get("art")))
|
||||
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
|
||||
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
|
||||
|
||||
def show_genre_list(menu_params):
|
||||
log.debug("showGenreList: {0}", menu_params)
|
||||
log.debug("showGenreList: {0}".format(menu_params))
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
if server is None:
|
||||
@@ -321,17 +323,17 @@ def show_genre_list(menu_params):
|
||||
|
||||
url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(url))))}
|
||||
item_data['art'] = art
|
||||
|
||||
item_data['path'] = url
|
||||
collections.append(item_data)
|
||||
|
||||
for collection in collections:
|
||||
url = sys.argv[0] + ("?url=" + urllib.quote(collection['path']) +
|
||||
url = sys.argv[0] + ("?url=" + quote(collection['path']) +
|
||||
"&mode=GET_CONTENT" +
|
||||
"&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} - {1} - {2}", collection.get('title'), url, collection.get("art"))
|
||||
log.debug("addMenuDirectoryItem: {0} - {1} - {2}".format(collection.get('title'), url, collection.get("art")))
|
||||
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
|
||||
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
@@ -360,22 +362,12 @@ def show_movie_alpha_list(menu_params):
|
||||
if parent_id is not None:
|
||||
url_params["ParentId"] = parent_id
|
||||
|
||||
prefix_url = get_jellyfin_url("{server}/Items/Prefixes", url_params)
|
||||
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(prefix_url)
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
alpha_list = []
|
||||
for prefix in result:
|
||||
alpha_list.append(prefix.get("Name"))
|
||||
prefixes = '#' + string.ascii_uppercase
|
||||
|
||||
collections = []
|
||||
for alphaName in alpha_list:
|
||||
for alpha_name in prefixes:
|
||||
item_data = {}
|
||||
item_data['title'] = alphaName
|
||||
item_data['title'] = alpha_name
|
||||
item_data['media_type'] = "Movies"
|
||||
|
||||
params = {}
|
||||
@@ -391,23 +383,23 @@ def show_movie_alpha_list(menu_params):
|
||||
if parent_id is not None:
|
||||
params["ParentId"] = parent_id
|
||||
|
||||
if alphaName == "#":
|
||||
if alpha_name == "#":
|
||||
params["NameLessThan"] = "A"
|
||||
else:
|
||||
params["NameStartsWith"] = alphaName
|
||||
params["NameStartsWith"] = alpha_name
|
||||
|
||||
url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
item_data['path'] = url
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(url))))}
|
||||
item_data['art'] = art
|
||||
|
||||
collections.append(item_data)
|
||||
|
||||
for collection in collections:
|
||||
url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) +
|
||||
url = (sys.argv[0] + "?url=" + quote(collection['path']) +
|
||||
"&mode=GET_CONTENT&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} ({1})", collection.get('title'), url)
|
||||
log.debug("addMenuDirectoryItem: {0} ({1})".format(collection.get('title'), url))
|
||||
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
|
||||
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
@@ -430,20 +422,11 @@ def show_tvshow_alpha_list(menu_params):
|
||||
url_params["SortOrder"] = "Ascending"
|
||||
if parent_id is not None:
|
||||
menu_params["ParentId"] = parent_id
|
||||
prefix_url = get_jellyfin_url("{server}/Items/Prefixes", url_params)
|
||||
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(prefix_url)
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
alpha_list = []
|
||||
for prefix in result:
|
||||
alpha_list.append(prefix.get("Name"))
|
||||
prefixes = '#' + string.ascii_uppercase
|
||||
|
||||
collections = []
|
||||
for alpha_name in alpha_list:
|
||||
for alpha_name in prefixes:
|
||||
item_data = {}
|
||||
item_data['title'] = alpha_name
|
||||
item_data['media_type'] = "tvshows"
|
||||
@@ -469,15 +452,15 @@ def show_tvshow_alpha_list(menu_params):
|
||||
|
||||
item_data['path'] = path
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(path)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(path))))}
|
||||
item_data['art'] = art
|
||||
|
||||
collections.append(item_data)
|
||||
|
||||
for collection in collections:
|
||||
url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) +
|
||||
url = (sys.argv[0] + "?url=" + quote(collection['path']) +
|
||||
"&mode=GET_CONTENT&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} ({1})", collection.get('title'), url)
|
||||
log.debug("addMenuDirectoryItem: {0} ({1})".format(collection.get('title'), url))
|
||||
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
|
||||
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
@@ -547,7 +530,7 @@ def display_homevideos_type(menu_params, view):
|
||||
base_params["Fields"] = "{field_filters}"
|
||||
base_params["ImageTypeLimit"] = 1
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", base_params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
|
||||
add_menu_directory_item(view_name + string_load(30405), url)
|
||||
|
||||
# In progress home movies
|
||||
@@ -557,7 +540,7 @@ def display_homevideos_type(menu_params, view):
|
||||
params["Recursive"] = True
|
||||
params["Limit"] = "{ItemLimit}"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
|
||||
add_menu_directory_item(view_name + string_load(30267) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# Recently added
|
||||
@@ -571,7 +554,7 @@ def display_homevideos_type(menu_params, view):
|
||||
params["IsPlayed"] = False
|
||||
params["Limit"] = "{ItemLimit}"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
|
||||
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
@@ -613,7 +596,7 @@ def display_tvshow_type(menu_params, view):
|
||||
base_params["IncludeItemTypes"] = "Series"
|
||||
base_params["Recursive"] = True
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", base_params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
add_menu_directory_item(view_name + string_load(30405), url)
|
||||
|
||||
# Favorite TV Shows
|
||||
@@ -621,7 +604,7 @@ def display_tvshow_type(menu_params, view):
|
||||
params.update(base_params)
|
||||
params["Filters"] = "IsFavorite"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
add_menu_directory_item(view_name + string_load(30414), url)
|
||||
|
||||
# Tv Shows with unplayed
|
||||
@@ -629,7 +612,7 @@ def display_tvshow_type(menu_params, view):
|
||||
params.update(base_params)
|
||||
params["IsPlayed"] = False
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
add_menu_directory_item(view_name + string_load(30285), url)
|
||||
|
||||
# In progress episodes
|
||||
@@ -641,8 +624,8 @@ def display_tvshow_type(menu_params, view):
|
||||
params["Filters"] = "IsResumable"
|
||||
params["IncludeItemTypes"] = "Episode"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
|
||||
url += "&name_format=" + urllib.quote('Episode|episode_name_format')
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
|
||||
url += "&name_format=" + quote('Episode|episode_name_format')
|
||||
add_menu_directory_item(view_name + string_load(30267) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# Latest Episodes
|
||||
@@ -653,7 +636,7 @@ def display_tvshow_type(menu_params, view):
|
||||
params["SortOrder"] = "Descending"
|
||||
params["IncludeItemTypes"] = "Episode"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items/Latest", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows&sort=none"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows&sort=none"
|
||||
add_menu_directory_item(view_name + string_load(30288) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# Recently Added
|
||||
@@ -665,8 +648,8 @@ def display_tvshow_type(menu_params, view):
|
||||
params["Filters"] = "IsNotFolder"
|
||||
params["IncludeItemTypes"] = "Episode"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
|
||||
url += "&name_format=" + urllib.quote('Episode|episode_name_format')
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
|
||||
url += "&name_format=" + quote('Episode|episode_name_format')
|
||||
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# Next Up Episodes
|
||||
@@ -679,8 +662,8 @@ def display_tvshow_type(menu_params, view):
|
||||
params["Filters"] = "IsNotFolder"
|
||||
params["IncludeItemTypes"] = "Episode"
|
||||
path = get_jellyfin_url("{server}/Shows/NextUp", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
|
||||
url += "&name_format=" + urllib.quote('Episode|episode_name_format')
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
|
||||
url += "&name_format=" + quote('Episode|episode_name_format')
|
||||
add_menu_directory_item(view_name + string_load(30278) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# TV Show Genres
|
||||
@@ -712,7 +695,7 @@ def display_music_type(menu_params, view):
|
||||
params["ImageTypeLimit"] = 1
|
||||
params["IncludeItemTypes"] = "MusicAlbum"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
|
||||
add_menu_directory_item(view_name + string_load(30320), url)
|
||||
|
||||
# recently added
|
||||
@@ -722,7 +705,7 @@ def display_music_type(menu_params, view):
|
||||
params["IncludeItemTypes"] = "Audio"
|
||||
params["Limit"] = "{ItemLimit}"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items/Latest", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
|
||||
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# recently played
|
||||
@@ -736,7 +719,7 @@ def display_music_type(menu_params, view):
|
||||
params["SortBy"] = "DatePlayed"
|
||||
params["SortOrder"] = "Descending"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
|
||||
add_menu_directory_item(view_name + string_load(30349) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# most played
|
||||
@@ -750,7 +733,7 @@ def display_music_type(menu_params, view):
|
||||
params["SortBy"] = "PlayCount"
|
||||
params["SortOrder"] = "Descending"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
|
||||
add_menu_directory_item(view_name + string_load(30353) + " (" + show_x_filtered_items + ")", url)
|
||||
|
||||
# artists
|
||||
@@ -759,7 +742,7 @@ def display_music_type(menu_params, view):
|
||||
params["Recursive"] = True
|
||||
params["ImageTypeLimit"] = 1
|
||||
path = get_jellyfin_url("{server}/Artists/AlbumArtists", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicArtists"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicArtists"
|
||||
add_menu_directory_item(view_name + string_load(30321), url)
|
||||
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
@@ -779,7 +762,7 @@ def display_musicvideos_type(params, view):
|
||||
params["IsMissing"] = False
|
||||
params["Fields"] = "{field_filters}"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=musicvideos"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=musicvideos"
|
||||
add_menu_directory_item(view_name + string_load(30405), url)
|
||||
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
@@ -798,7 +781,7 @@ def display_livetv_type(menu_params, view):
|
||||
params["ImageTypeLimit"] = 1
|
||||
params["Fields"] = "{field_filters}"
|
||||
path = get_jellyfin_url("{server}/LiveTv/Channels", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=livetv"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=livetv"
|
||||
add_menu_directory_item(view_name + string_load(30360), url)
|
||||
|
||||
# programs
|
||||
@@ -809,7 +792,7 @@ def display_livetv_type(menu_params, view):
|
||||
params["Fields"] = "ChannelInfo,{field_filters}"
|
||||
params["EnableTotalRecordCount"] = False
|
||||
path = get_jellyfin_url("{server}/LiveTv/Programs/Recommended", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=livetv"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=livetv"
|
||||
add_menu_directory_item(view_name + string_load(30361), url)
|
||||
|
||||
# recordings
|
||||
@@ -820,7 +803,7 @@ def display_livetv_type(menu_params, view):
|
||||
params["Fields"] = "{field_filters}"
|
||||
params["EnableTotalRecordCount"] = False
|
||||
path = get_jellyfin_url("{server}/LiveTv/Recordings", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=livetv"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=livetv"
|
||||
add_menu_directory_item(view_name + string_load(30362), url)
|
||||
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
@@ -852,8 +835,8 @@ def display_movies_type(menu_params, view):
|
||||
|
||||
# All Movies
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", base_params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies"
|
||||
add_menu_directory_item(view_name + string_load(30405), url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies"
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30405)), url)
|
||||
|
||||
# Favorite Movies
|
||||
params = {}
|
||||
@@ -862,8 +845,8 @@ def display_movies_type(menu_params, view):
|
||||
params["GroupItemsIntoCollections"] = False
|
||||
params["Filters"] = "IsFavorite"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies"
|
||||
add_menu_directory_item(view_name + string_load(30414), url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies"
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30414)), url)
|
||||
|
||||
# Unwatched Movies
|
||||
params = {}
|
||||
@@ -872,8 +855,8 @@ def display_movies_type(menu_params, view):
|
||||
params["GroupItemsIntoCollections"] = False
|
||||
params["IsPlayed"] = False
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies"
|
||||
add_menu_directory_item(view_name + string_load(30285), url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies"
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30285)), url)
|
||||
|
||||
# Recently Watched Movies
|
||||
params = {}
|
||||
@@ -885,8 +868,8 @@ def display_movies_type(menu_params, view):
|
||||
params["GroupItemsIntoCollections"] = False
|
||||
params["Limit"] = "{ItemLimit}"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
|
||||
add_menu_directory_item(view_name + string_load(30349) + " (" + show_x_filtered_items + ")", url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
|
||||
add_menu_directory_item('{}{} ({})'.format(view_name, string_load(30349), show_x_filtered_items), url)
|
||||
|
||||
# Resumable Movies
|
||||
params = {}
|
||||
@@ -896,8 +879,8 @@ def display_movies_type(menu_params, view):
|
||||
params["SortOrder"] = "Descending"
|
||||
params["Limit"] = "{ItemLimit}"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
|
||||
add_menu_directory_item(view_name + string_load(30267) + " (" + show_x_filtered_items + ")", url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
|
||||
add_menu_directory_item('{}{} ({})'.format(view_name, string_load(30267), show_x_filtered_items), url)
|
||||
|
||||
# Recently Added Movies
|
||||
params = {}
|
||||
@@ -908,8 +891,8 @@ def display_movies_type(menu_params, view):
|
||||
params["SortOrder"] = "Descending"
|
||||
params["Filters"] = "IsNotFolder"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
|
||||
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
|
||||
add_menu_directory_item('{}{} ({})'.format(view_name, string_load(30268), show_x_filtered_items), url)
|
||||
|
||||
# Collections
|
||||
params = {}
|
||||
@@ -920,50 +903,50 @@ def display_movies_type(menu_params, view):
|
||||
params["IncludeItemTypes"] = "Boxset"
|
||||
params["Recursive"] = True
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
|
||||
add_menu_directory_item(view_name + string_load(30410), url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30410)), url)
|
||||
|
||||
# Favorite Collections
|
||||
params["Filters"] = "IsFavorite"
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
|
||||
add_menu_directory_item(view_name + string_load(30415), url)
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30415)), url)
|
||||
|
||||
# Genres
|
||||
path = "plugin://plugin.video.jellycon/?mode=GENRES&item_type=movie"
|
||||
if view is not None:
|
||||
path += "&parent_id=" + view.get("Id")
|
||||
add_menu_directory_item(view_name + string_load(30325), path)
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30325)), path)
|
||||
|
||||
# Pages
|
||||
path = "plugin://plugin.video.jellycon/?mode=MOVIE_PAGES"
|
||||
if view is not None:
|
||||
path += "&parent_id=" + view.get("Id")
|
||||
add_menu_directory_item(view_name + string_load(30397), path)
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30397)), path)
|
||||
|
||||
# Alpha Picker
|
||||
path = "plugin://plugin.video.jellycon/?mode=MOVIE_ALPHA"
|
||||
if view is not None:
|
||||
path += "&parent_id=" + view.get("Id")
|
||||
add_menu_directory_item(view_name + string_load(30404), path)
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30404)), path)
|
||||
|
||||
# Years
|
||||
path = "plugin://plugin.video.jellycon/?mode=SHOW_ADDON_MENU&type=show_movie_years"
|
||||
if view is not None:
|
||||
path += "&parent_id=" + view.get("Id")
|
||||
add_menu_directory_item(view_name + string_load(30411), path)
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30411)), path)
|
||||
|
||||
# Decades
|
||||
path = "plugin://plugin.video.jellycon/?mode=SHOW_ADDON_MENU&type=show_movie_years&group=true"
|
||||
if view is not None:
|
||||
path += "&parent_id=" + view.get("Id")
|
||||
add_menu_directory_item(view_name + string_load(30412), path)
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30412)), path)
|
||||
|
||||
# Tags
|
||||
path = "plugin://plugin.video.jellycon/?mode=SHOW_ADDON_MENU&type=show_movie_tags"
|
||||
if view is not None:
|
||||
path += "&parent_id=" + view.get("Id")
|
||||
add_menu_directory_item(view_name + string_load(30413), path)
|
||||
add_menu_directory_item('{}{}'.format(view_name, string_load(30413)), path)
|
||||
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
|
||||
@@ -1014,7 +997,7 @@ def get_playlist_path(view_info):
|
||||
params["ImageTypeLimit"] = 1
|
||||
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=playlists"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=playlists"
|
||||
return url
|
||||
|
||||
|
||||
@@ -1030,7 +1013,7 @@ def get_collection_path(view_info):
|
||||
params["IsMissing"] = False
|
||||
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
|
||||
return url
|
||||
|
||||
|
||||
@@ -1042,7 +1025,7 @@ def get_channel_path(view):
|
||||
params["Fields"] = "{field_filters}"
|
||||
|
||||
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=files"
|
||||
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=files"
|
||||
return url
|
||||
|
||||
|
||||
@@ -1053,7 +1036,7 @@ def display_library_view(params):
|
||||
data_manager = DataManager()
|
||||
view_info = data_manager.get_content(view_info_url)
|
||||
|
||||
log.debug("VIEW_INFO : {0}", view_info)
|
||||
log.debug("VIEW_INFO : {0}".format(view_info))
|
||||
|
||||
collection_type = view_info.get("CollectionType", None)
|
||||
|
||||
@@ -1111,7 +1094,7 @@ def show_search():
|
||||
|
||||
|
||||
def set_library_window_values(force=False):
|
||||
log.debug("set_library_window_values Called forced={0}", force)
|
||||
log.debug("set_library_window_values Called forced={0}".format(force))
|
||||
home_window = HomeWindow()
|
||||
|
||||
already_set = home_window.get_property("view_item.0.name")
|
||||
@@ -1145,19 +1128,19 @@ def set_library_window_values(force=False):
|
||||
# plugin.video.jellycon-
|
||||
prop_name = "view_item.%i.name" % index
|
||||
home_window.set_property(prop_name, name)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}", prop_name, name)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}".format(prop_name, name))
|
||||
|
||||
prop_name = "view_item.%i.id" % index
|
||||
home_window.set_property(prop_name, item_id)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}", prop_name, item_id)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}".format(prop_name, item_id))
|
||||
|
||||
prop_name = "view_item.%i.type" % index
|
||||
home_window.set_property(prop_name, collection_type)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}", prop_name, collection_type)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}".format(prop_name, collection_type))
|
||||
|
||||
thumb = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
prop_name = "view_item.%i.thumb" % index
|
||||
home_window.set_property(prop_name, thumb)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}", prop_name, thumb)
|
||||
log.debug("set_library_window_values: plugin.video.jellycon-{0}={1}".format(prop_name, thumb))
|
||||
|
||||
index += 1
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
class PictureViewer(xbmcgui.WindowXMLDialog):
|
||||
picture_url = None
|
||||
@@ -21,11 +23,6 @@ class PictureViewer(xbmcgui.WindowXMLDialog):
|
||||
picture_control = self.getControl(3010)
|
||||
|
||||
picture_control.setImage(self.picture_url)
|
||||
# self.listControl.addItems(self.action_items)
|
||||
# self.setFocus(self.listControl)
|
||||
|
||||
# bg_image = self.getControl(3010)
|
||||
# bg_image.setHeight(50 * len(self.action_items) + 20)
|
||||
|
||||
def onFocus(self, controlId):
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
@@ -8,8 +9,9 @@ from datetime import timedelta
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .downloadutils import DownloadUtils
|
||||
from .resume_dialog import ResumeDialog
|
||||
from .utils import PlayUtils, get_art, send_event_notification, convert_size
|
||||
@@ -18,23 +20,25 @@ from .translation import string_load
|
||||
from .datamanager import DataManager, clear_old_cache_data
|
||||
from .item_functions import extract_item_info, add_gui_item
|
||||
from .clientinfo import ClientInformation
|
||||
from .functions import delete
|
||||
from .cache_images import CacheArtwork
|
||||
from .picture_viewer import PictureViewer
|
||||
from .tracking import timer
|
||||
from .playnext import PlayNextDialog
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
download_utils = DownloadUtils()
|
||||
|
||||
|
||||
def play_all_files(items, monitor, play_items=True):
|
||||
def play_all_files(items, play_items=True):
|
||||
home_window = HomeWindow()
|
||||
log.debug("playAllFiles called with items: {0}", items)
|
||||
server = download_utils.get_server()
|
||||
|
||||
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
playlist.clear()
|
||||
|
||||
playlist_data = {}
|
||||
|
||||
for item in items:
|
||||
|
||||
item_id = item.get("Id")
|
||||
@@ -60,7 +64,7 @@ def play_all_files(items, monitor, play_items=True):
|
||||
source_id = selected_media_source.get("Id")
|
||||
|
||||
playurl, playback_type, listitem_props = PlayUtils().get_play_url(selected_media_source, play_session_id)
|
||||
log.info("Play URL: {0} PlaybackType: {1} ListItem Properties: {2}", playurl, playback_type, listitem_props)
|
||||
log.info("Play URL: {0} PlaybackType: {1} ListItem Properties: {2}".format(playurl, playback_type, listitem_props))
|
||||
|
||||
if playurl is None:
|
||||
return
|
||||
@@ -82,14 +86,17 @@ def play_all_files(items, monitor, play_items=True):
|
||||
list_item = xbmcgui.ListItem(label=item_title)
|
||||
|
||||
# add playurl and data to the monitor
|
||||
data = {}
|
||||
data["item_id"] = item_id
|
||||
data["source_id"] = source_id
|
||||
data["playback_type"] = playback_type_string
|
||||
data["play_session_id"] = play_session_id
|
||||
data["play_action_type"] = "play_all"
|
||||
monitor.played_information[playurl] = data
|
||||
log.debug("Add to played_information: {0}", monitor.played_information)
|
||||
playlist_data[playurl] = {}
|
||||
playlist_data[playurl]["item_id"] = item_id
|
||||
playlist_data[playurl]["source_id"] = source_id
|
||||
playlist_data[playurl]["playback_type"] = playback_type_string
|
||||
playlist_data[playurl]["play_session_id"] = play_session_id
|
||||
playlist_data[playurl]["play_action_type"] = "play_all"
|
||||
home_window.set_property('playlist', json.dumps(playlist_data))
|
||||
|
||||
# Set now_playing to the first track
|
||||
if len(playlist_data) == 1:
|
||||
home_window.set_property('now_playing', json.dumps(playlist_data[playurl]))
|
||||
|
||||
list_item.setPath(playurl)
|
||||
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
|
||||
@@ -103,7 +110,7 @@ def play_all_files(items, monitor, play_items=True):
|
||||
return playlist
|
||||
|
||||
|
||||
def play_list_of_items(id_list, monitor):
|
||||
def play_list_of_items(id_list):
|
||||
log.debug("Loading all items in the list")
|
||||
data_manager = DataManager()
|
||||
items = []
|
||||
@@ -117,11 +124,11 @@ def play_list_of_items(id_list, monitor):
|
||||
return
|
||||
items.append(result)
|
||||
|
||||
return play_all_files(items, monitor)
|
||||
return play_all_files(items)
|
||||
|
||||
|
||||
def add_to_playlist(play_info, monitor):
|
||||
log.debug("Adding item to playlist : {0}", play_info)
|
||||
def add_to_playlist(play_info):
|
||||
log.debug("Adding item to playlist : {0}".format(play_info))
|
||||
|
||||
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
server = download_utils.get_server()
|
||||
@@ -152,14 +159,13 @@ def add_to_playlist(play_info, monitor):
|
||||
play_session_id = playback_info.get("PlaySessionId")
|
||||
|
||||
# select the media source to use
|
||||
# sources = item.get("MediaSources")
|
||||
sources = playback_info.get('MediaSources')
|
||||
|
||||
selected_media_source = sources[0]
|
||||
source_id = selected_media_source.get("Id")
|
||||
|
||||
playurl, playback_type, listitem_props = PlayUtils().get_play_url(selected_media_source, play_session_id)
|
||||
log.info("Play URL: {0} PlaybackType: {1} ListItem Properties: {2}", playurl, playback_type, listitem_props)
|
||||
log.info("Play URL: {0} PlaybackType: {1} ListItem Properties: {2}".format(playurl, playback_type, listitem_props))
|
||||
|
||||
if playurl is None:
|
||||
return
|
||||
@@ -187,8 +193,6 @@ def add_to_playlist(play_info, monitor):
|
||||
data["playback_type"] = playback_type_string
|
||||
data["play_session_id"] = play_session_id
|
||||
data["play_action_type"] = "play_all"
|
||||
monitor.played_information[playurl] = data
|
||||
log.debug("Add to played_information: {0}", monitor.played_information)
|
||||
|
||||
list_item.setPath(playurl)
|
||||
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
|
||||
@@ -215,7 +219,7 @@ def get_playback_intros(item_id):
|
||||
|
||||
|
||||
@timer
|
||||
def play_file(play_info, monitor):
|
||||
def play_file(play_info):
|
||||
item_id = play_info.get("item_id")
|
||||
|
||||
home_window = HomeWindow()
|
||||
@@ -225,12 +229,12 @@ def play_file(play_info, monitor):
|
||||
|
||||
action = play_info.get("action", "play")
|
||||
if action == "add_to_playlist":
|
||||
add_to_playlist(play_info, monitor)
|
||||
add_to_playlist(play_info)
|
||||
return
|
||||
|
||||
# if this is a list of items them add them all to the play list
|
||||
if isinstance(item_id, list):
|
||||
return play_list_of_items(item_id, monitor)
|
||||
return play_list_of_items(item_id)
|
||||
|
||||
auto_resume = play_info.get("auto_resume", "-1")
|
||||
force_transcode = play_info.get("force_transcode", False)
|
||||
@@ -238,7 +242,7 @@ def play_file(play_info, monitor):
|
||||
subtitle_stream_index = play_info.get("subtitle_stream_index", None)
|
||||
audio_stream_index = play_info.get("audio_stream_index", None)
|
||||
|
||||
log.debug("playFile id({0}) resume({1}) force_transcode({2})", item_id, auto_resume, force_transcode)
|
||||
log.debug("playFile id({0}) resume({1}) force_transcode({2})".format(item_id, auto_resume, force_transcode))
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
addon_path = settings.getAddonInfo('path')
|
||||
@@ -251,7 +255,7 @@ def play_file(play_info, monitor):
|
||||
url = "{server}/Users/{userid}/Items/%s?format=json" % (item_id,)
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(url)
|
||||
log.debug("Playfile item: {0}", result)
|
||||
log.debug("Playfile item: {0}".format(result))
|
||||
|
||||
if result is None:
|
||||
log.debug("Playfile item was None, so can not play!")
|
||||
@@ -259,20 +263,20 @@ def play_file(play_info, monitor):
|
||||
|
||||
# if this is a season, playlist or album then play all items in that parent
|
||||
if result.get("Type") in ["Season", "MusicAlbum", "Playlist"]:
|
||||
log.debug("PlayAllFiles for parent item id: {0}", item_id)
|
||||
log.debug("PlayAllFiles for parent item id: {0}".format(item_id))
|
||||
url = ('{server}/Users/{userid}/items' +
|
||||
'?ParentId=%s' +
|
||||
'&Fields=MediaSources' +
|
||||
'&format=json')
|
||||
url = url % (item_id,)
|
||||
result = data_manager.get_content(url)
|
||||
log.debug("PlayAllFiles items: {0}", result)
|
||||
log.debug("PlayAllFiles items: {0}".format(result))
|
||||
|
||||
# process each item
|
||||
items = result["Items"]
|
||||
if items is None:
|
||||
items = []
|
||||
return play_all_files(items, monitor)
|
||||
return play_all_files(items)
|
||||
|
||||
# if this is a program from live tv epg then play the actual channel
|
||||
if result.get("Type") == "Program":
|
||||
@@ -366,32 +370,16 @@ def play_file(play_info, monitor):
|
||||
resume_dialog.doModal()
|
||||
resume_result = resume_dialog.getResumeAction()
|
||||
del resume_dialog
|
||||
log.debug("Resume Dialog Result: {0}", resume_result)
|
||||
|
||||
# check system settings for play action
|
||||
# if prompt is set ask to set it to auto resume
|
||||
# remove for now as the context dialog is now handeled in the monitor thread
|
||||
# params = {"setting": "myvideos.selectaction"}
|
||||
# setting_result = json_rpc('Settings.getSettingValue').execute(params)
|
||||
# log.debug("Current Setting (myvideos.selectaction): {0}", setting_result)
|
||||
# current_value = setting_result.get("result", None)
|
||||
# if current_value is not None:
|
||||
# current_value = current_value.get("value", -1)
|
||||
# if current_value not in (2,3):
|
||||
# return_value = xbmcgui.Dialog().yesno(string_load(30276), string_load(30277))
|
||||
# if return_value:
|
||||
# params = {"setting": "myvideos.selectaction", "value": 2}
|
||||
# json_rpc_result = json_rpc('Settings.setSettingValue').execute(params)
|
||||
# log.debug("Save Setting (myvideos.selectaction): {0}", json_rpc_result)
|
||||
log.debug("Resume Dialog Result: {0}".format(resume_result))
|
||||
|
||||
if resume_result == 1:
|
||||
seek_time = 0
|
||||
elif resume_result == -1:
|
||||
return
|
||||
|
||||
log.debug("play_session_id: {0}", play_session_id)
|
||||
log.debug("play_session_id: {0}".format(play_session_id))
|
||||
playurl, playback_type, listitem_props = PlayUtils().get_play_url(selected_media_source, play_session_id)
|
||||
log.info("Play URL: {0} Playback Type: {1} ListItem Properties: {2}", playurl, playback_type, listitem_props)
|
||||
log.info("Play URL: {0} Playback Type: {1} ListItem Properties: {2}".format(playurl, playback_type, listitem_props))
|
||||
|
||||
if playurl is None:
|
||||
return
|
||||
@@ -431,7 +419,7 @@ def play_file(play_info, monitor):
|
||||
if playback_type == "2": # if transcoding then prompt for audio and subtitle
|
||||
playurl = audio_subs_pref(playurl, list_item, selected_media_source, item_id, audio_stream_index,
|
||||
subtitle_stream_index)
|
||||
log.debug("New playurl for transcoding: {0}", playurl)
|
||||
log.debug("New playurl for transcoding: {0}".format(playurl))
|
||||
|
||||
elif playback_type == "1": # for direct stream add any streamable subtitles
|
||||
external_subs(selected_media_source, list_item, item_id)
|
||||
@@ -445,8 +433,13 @@ def play_file(play_info, monitor):
|
||||
data["play_action_type"] = "play"
|
||||
data["item_type"] = result.get("Type", None)
|
||||
data["can_delete"] = result.get("CanDelete", False)
|
||||
monitor.played_information[playurl] = data
|
||||
log.debug("Add to played_information: {0}", monitor.played_information)
|
||||
|
||||
# Check for next episodes
|
||||
if result.get('Type') == 'Episode':
|
||||
next_episode = get_next_episode(result)
|
||||
data["next_episode"] = next_episode
|
||||
|
||||
home_window.set_property('now_playing', json.dumps(data))
|
||||
|
||||
list_item.setPath(playurl)
|
||||
list_item = set_list_item_props(item_id, list_item, result, server, listitem_props, item_title)
|
||||
@@ -458,7 +451,7 @@ def play_file(play_info, monitor):
|
||||
intro_items = get_playback_intros(item_id)
|
||||
|
||||
if len(intro_items) > 0:
|
||||
playlist = play_all_files(intro_items, monitor, play_items=False)
|
||||
playlist = play_all_files(intro_items, play_items=False)
|
||||
playlist.add(playurl, list_item)
|
||||
else:
|
||||
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
@@ -488,12 +481,12 @@ def play_file(play_info, monitor):
|
||||
count = 0
|
||||
max_loops = 2 * 120
|
||||
while not monitor.abortRequested() and player.isPlaying() and count < max_loops:
|
||||
log.info("PlaybackResumrAction : Seeking to : {0}", seek_to_time)
|
||||
log.info("PlaybackResumrAction : Seeking to : {0}".format(seek_to_time))
|
||||
player.seekTime(seek_to_time)
|
||||
current_position = player.getTime()
|
||||
if current_position >= target_seek:
|
||||
break
|
||||
log.info("PlaybackResumrAction : target:{0} current:{1}", target_seek, current_position)
|
||||
log.info("PlaybackResumrAction : target:{0} current:{1}".format(target_seek, current_position))
|
||||
count = count + 1
|
||||
xbmc.sleep(500)
|
||||
|
||||
@@ -513,10 +506,6 @@ def play_file(play_info, monitor):
|
||||
else:
|
||||
log.info("PlaybackResumrAction : Playback resumed")
|
||||
|
||||
next_episode = get_next_episode(result)
|
||||
data["next_episode"] = next_episode
|
||||
send_next_episode_details(result, next_episode)
|
||||
|
||||
|
||||
def __build_label2_from(source):
|
||||
videos = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Video"]
|
||||
@@ -524,7 +513,6 @@ def __build_label2_from(source):
|
||||
subtitles = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Subtitle"]
|
||||
|
||||
details = [str(convert_size(source.get('Size', 0)))]
|
||||
# details.append(source.get('Container', ''))
|
||||
for video in videos:
|
||||
details.append('{} {} {}bit'.format(video.get('DisplayTitle', ''),
|
||||
video.get('VideoRange', ''),
|
||||
@@ -545,18 +533,18 @@ def __build_label2_from(source):
|
||||
|
||||
|
||||
def get_next_episode(item):
|
||||
if item.get("Type", "na") != "Episode":
|
||||
if item.get("Type") != "Episode":
|
||||
log.debug("Not an episode, can not get next")
|
||||
return None
|
||||
|
||||
parent_id = item.get("ParentId", "na")
|
||||
item_index = item.get("IndexNumber", -1)
|
||||
parent_id = item.get("ParentId")
|
||||
item_index = item.get("IndexNumber")
|
||||
|
||||
if parent_id == "na":
|
||||
if parent_id is None:
|
||||
log.debug("No parent id, can not get next")
|
||||
return None
|
||||
|
||||
if item_index == -1:
|
||||
if item_index is None:
|
||||
log.debug("No episode number, can not get next")
|
||||
return None
|
||||
|
||||
@@ -571,19 +559,18 @@ def get_next_episode(item):
|
||||
|
||||
data_manager = DataManager()
|
||||
items_result = data_manager.get_content(url)
|
||||
log.debug("get_next_episode, sibling list: {0}", items_result)
|
||||
log.debug("get_next_episode, sibling list: {0}".format(items_result))
|
||||
|
||||
if items_result is None:
|
||||
log.debug("get_next_episode no results")
|
||||
return None
|
||||
|
||||
item_list = items_result.get("Items", [])
|
||||
item_list = items_result.get("Items") or []
|
||||
|
||||
for item in item_list:
|
||||
index = item.get("IndexNumber", -1)
|
||||
# find the very next episode in the season
|
||||
if index == item_index + 1:
|
||||
log.debug("get_next_episode, found next episode: {0}", item)
|
||||
if item.get("IndexNumber") == item_index + 1:
|
||||
log.debug("get_next_episode, found next episode: {0}".format(item))
|
||||
return item
|
||||
|
||||
return None
|
||||
@@ -650,16 +637,13 @@ def send_next_episode_details(item, next_episode):
|
||||
"force_transcode": False
|
||||
}
|
||||
}
|
||||
send_event_notification("upnext_data", next_info)
|
||||
send_event_notification("upnext_data", next_info, True)
|
||||
|
||||
|
||||
def set_list_item_props(item_id, list_item, result, server, extra_props, title):
|
||||
# set up item and item info
|
||||
|
||||
art = get_art(result, server=server)
|
||||
list_item.setIconImage(art['thumb']) # back compat
|
||||
list_item.setProperty('fanart_image', art['fanart']) # back compat
|
||||
list_item.setProperty('discart', art['discart']) # not avail to setArt
|
||||
list_item.setArt(art)
|
||||
|
||||
list_item.setProperty('IsPlayable', 'false')
|
||||
@@ -795,7 +779,7 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s
|
||||
if select_subs_index in downloadable_streams:
|
||||
subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
|
||||
subtitle_url = subtitle_url % (download_utils.get_server(), item_id, source_id, select_subs_index)
|
||||
log.debug("Streaming subtitles url: {0} {1}", select_subs_index, subtitle_url)
|
||||
log.debug("Streaming subtitles url: {0} {1}".format(select_subs_index, subtitle_url))
|
||||
list_item.setSubtitles([subtitle_url])
|
||||
else:
|
||||
# Burn subtitles
|
||||
@@ -815,7 +799,7 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s
|
||||
if select_subs_index in downloadable_streams:
|
||||
subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
|
||||
subtitle_url = subtitle_url % (download_utils.get_server(), item_id, source_id, select_subs_index)
|
||||
log.debug("Streaming subtitles url: {0} {1}", select_subs_index, subtitle_url)
|
||||
log.debug("Streaming subtitles url: {0} {1}".format(select_subs_index, subtitle_url))
|
||||
list_item.setSubtitles([subtitle_url])
|
||||
else:
|
||||
# Burn subtitles
|
||||
@@ -853,12 +837,15 @@ def external_subs(media_source, list_item, item_id):
|
||||
source_id = media_source['Id']
|
||||
server = download_utils.get_server()
|
||||
token = download_utils.authenticate()
|
||||
language = stream.get('Language', '')
|
||||
codec = stream.get('Codec', '')
|
||||
|
||||
if stream.get('DeliveryUrl', '').lower().startswith('/videos'):
|
||||
url = "%s%s" % (server, stream.get('DeliveryUrl'))
|
||||
url_root = '{}/Videos/{}/{}/Subtitles/{}'.format(server, item_id, source_id, index)
|
||||
if language:
|
||||
url = '{}/0/Stream.{}.{}?api_key={}'.format(
|
||||
url_root, language, codec, token)
|
||||
else:
|
||||
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s"
|
||||
% (server, item_id, source_id, index, stream['Codec'], token))
|
||||
url = '{}/0/Stream.{}?api_key={}'.format(url_root, codec, token)
|
||||
|
||||
default = ""
|
||||
if stream['IsDefault']:
|
||||
@@ -867,7 +854,7 @@ def external_subs(media_source, list_item, item_id):
|
||||
if stream['IsForced']:
|
||||
forced = "forced"
|
||||
|
||||
sub_name = stream.get('Language', "n/a") + " (" + stream.get('Codec', "n/a") + ") " + default + " " + forced
|
||||
sub_name = '{} ( {} ) {} {}'.format(language, codec, default, forced)
|
||||
|
||||
sub_names.append(sub_name)
|
||||
externalsubs.append(url)
|
||||
@@ -884,12 +871,14 @@ def external_subs(media_source, list_item, item_id):
|
||||
resp = xbmcgui.Dialog().select(string_load(30292), sub_names)
|
||||
if resp > -1:
|
||||
selected_sub = externalsubs[resp]
|
||||
log.debug("External Subtitle Selected: {0}", selected_sub)
|
||||
log.debug("External Subtitle Selected: {0}".format(selected_sub))
|
||||
list_item.setSubtitles([selected_sub])
|
||||
|
||||
|
||||
def send_progress(monitor):
|
||||
play_data = get_playing_data(monitor.played_information)
|
||||
def send_progress():
|
||||
home_window = HomeWindow()
|
||||
play_data_string = home_window.get_property('now_playing')
|
||||
play_data = json.loads(play_data_string)
|
||||
|
||||
if play_data is None:
|
||||
return
|
||||
@@ -897,15 +886,18 @@ def send_progress(monitor):
|
||||
log.debug("Sending Progress Update")
|
||||
|
||||
player = xbmc.Player()
|
||||
item_id = play_data.get("item_id")
|
||||
|
||||
if item_id is None:
|
||||
return
|
||||
|
||||
play_time = player.getTime()
|
||||
total_play_time = player.getTotalTime()
|
||||
play_data["currentPossition"] = play_time
|
||||
play_data["current_position"] = play_time
|
||||
play_data["duration"] = total_play_time
|
||||
play_data["currently_playing"] = True
|
||||
|
||||
item_id = play_data.get("item_id")
|
||||
if item_id is None:
|
||||
return
|
||||
home_window.set_property('now_playing', json.dumps(play_data))
|
||||
|
||||
source_id = play_data.get("source_id")
|
||||
|
||||
@@ -937,7 +929,7 @@ def send_progress(monitor):
|
||||
'VolumeLevel': volume
|
||||
}
|
||||
|
||||
log.debug("Sending POST progress started: {0}", postdata)
|
||||
log.debug("Sending POST progress started: {0}".format(postdata))
|
||||
|
||||
url = "{server}/Sessions/Playing/Progress"
|
||||
download_utils.download_url(url, post_body=postdata, method="POST")
|
||||
@@ -955,10 +947,10 @@ def get_volume():
|
||||
|
||||
|
||||
def prompt_for_stop_actions(item_id, data):
|
||||
log.debug("prompt_for_stop_actions Called : {0}", data)
|
||||
log.debug("prompt_for_stop_actions Called : {0}".format(data))
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
current_position = data.get("currentPossition", 0)
|
||||
current_position = data.get("current_position", 0)
|
||||
duration = data.get("duration", 0)
|
||||
# media_source_id = data.get("source_id")
|
||||
next_episode = data.get("next_episode")
|
||||
@@ -984,9 +976,8 @@ def prompt_for_stop_actions(item_id, data):
|
||||
return
|
||||
|
||||
# item percentage complete
|
||||
# percenatge_complete = int(((current_position * 10000000) / runtime) * 100)
|
||||
percenatge_complete = int((current_position / duration) * 100)
|
||||
log.debug("Episode Percentage Complete: {0}", percenatge_complete)
|
||||
log.debug("Episode Percentage Complete: {0}".format(percenatge_complete))
|
||||
|
||||
if (can_delete and
|
||||
prompt_delete_episode_percentage < 100 and
|
||||
@@ -1000,21 +991,14 @@ def prompt_for_stop_actions(item_id, data):
|
||||
percenatge_complete > prompt_delete_movie_percentage):
|
||||
prompt_to_delete = True
|
||||
|
||||
if prompt_to_delete:
|
||||
log.debug("Prompting for delete")
|
||||
delete(item_id)
|
||||
|
||||
# prompt for next episode
|
||||
if (next_episode is not None and
|
||||
prompt_next_percentage < 100 and
|
||||
item_type == "Episode" and
|
||||
percenatge_complete > prompt_next_percentage):
|
||||
|
||||
# resp = True
|
||||
index = next_episode.get("IndexNumber", -1)
|
||||
if play_prompt:
|
||||
# series_name = next_episode.get("SeriesName")
|
||||
# next_epp_name = "Episode %02d - (%s)" % (index, next_episode.get("Name", "n/a"))
|
||||
|
||||
plugin_path = settings.getAddonInfo('path')
|
||||
plugin_path_real = xbmc.translatePath(os.path.join(plugin_path))
|
||||
@@ -1026,51 +1010,32 @@ def prompt_for_stop_actions(item_id, data):
|
||||
if not play_next_dialog.get_play_called():
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
# resp = xbmcgui.Dialog().yesno(string_load(30283),
|
||||
# series_name,
|
||||
# next_epp_name,
|
||||
# autoclose=20000)
|
||||
"""
|
||||
if resp:
|
||||
next_item_id = next_episode.get("Id")
|
||||
log.debug("Playing Next Episode: {0}", next_item_id)
|
||||
|
||||
play_info = {}
|
||||
play_info["item_id"] = next_item_id
|
||||
play_info["auto_resume"] = "-1"
|
||||
play_info["force_transcode"] = False
|
||||
send_event_notification("jellycon_play_action", play_info)
|
||||
|
||||
else:
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
"""
|
||||
|
||||
|
||||
def stop_all_playback(played_information):
|
||||
log.debug("stop_all_playback : {0}", played_information)
|
||||
log.debug("stop_all_playback : {0}".format(played_information))
|
||||
|
||||
if len(played_information) == 0:
|
||||
return
|
||||
|
||||
log.debug("played_information: {0}", played_information)
|
||||
log.debug("played_information: {0}".format(played_information))
|
||||
clear_entries = []
|
||||
|
||||
home_screen = HomeWindow()
|
||||
home_screen.clear_property("currently_playing_id")
|
||||
|
||||
for item_url in played_information:
|
||||
data = played_information.get(item_url)
|
||||
for item in played_information:
|
||||
data = played_information.get(item)
|
||||
if data.get("currently_playing", False) is True:
|
||||
log.debug("item_url: {0}", item_url)
|
||||
log.debug("item_data: {0}", data)
|
||||
log.debug("item_data: {0}".format(data))
|
||||
|
||||
current_position = data.get("currentPossition", 0)
|
||||
current_position = data.get("current_position", 0)
|
||||
duration = data.get("duration", 0)
|
||||
jellyfin_item_id = data.get("item_id")
|
||||
jellyfin_source_id = data.get("source_id")
|
||||
play_session_id = data.get("play_session_id")
|
||||
|
||||
if jellyfin_item_id is not None and current_position >= 0:
|
||||
log.debug("Playback Stopped at: {0}", current_position)
|
||||
log.debug("Playback Stopped at: {0}".format(current_position))
|
||||
|
||||
url = "{server}/Sessions/Playing/Stopped"
|
||||
postdata = {
|
||||
@@ -1086,34 +1051,51 @@ def stop_all_playback(played_information):
|
||||
if data.get("play_action_type", "") == "play":
|
||||
prompt_for_stop_actions(jellyfin_item_id, data)
|
||||
|
||||
device_id = ClientInformation().get_device_id()
|
||||
url = "{server}/Videos/ActiveEncodings?DeviceId=%s" % device_id
|
||||
download_utils.download_url(url, method="DELETE")
|
||||
clear_entries.append(item)
|
||||
|
||||
if data.get('playback_type') == 'Transcode':
|
||||
device_id = ClientInformation().get_device_id()
|
||||
url = "{server}/Videos/ActiveEncodings?DeviceId=%s" % device_id
|
||||
download_utils.download_url(url, method="DELETE")
|
||||
|
||||
for entry in clear_entries:
|
||||
del played_information[entry]
|
||||
|
||||
|
||||
def get_playing_data(play_data_map):
|
||||
def get_playing_data():
|
||||
home_window = HomeWindow()
|
||||
play_data_string = home_window.get_property('now_playing')
|
||||
play_data = json.loads(play_data_string)
|
||||
playlist_data_string = home_window.get_property('playlist')
|
||||
playlist_data = json.loads(playlist_data_string)
|
||||
item_id = play_data.get("item_id")
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
try:
|
||||
playing_file = xbmc.Player().getPlayingFile()
|
||||
except Exception as e:
|
||||
log.error("get_playing_data : getPlayingFile() : {0}", e)
|
||||
log.error("get_playing_data : getPlayingFile() : {0}".format(e))
|
||||
return None
|
||||
log.debug("get_playing_data : getPlayingFile() : {0}", playing_file)
|
||||
if playing_file not in play_data_map:
|
||||
infolabel_path_and_file = xbmc.getInfoLabel("Player.Filenameandpath")
|
||||
log.debug("get_playing_data : Filenameandpath : {0}", infolabel_path_and_file)
|
||||
if infolabel_path_and_file not in play_data_map:
|
||||
log.debug("get_playing_data : play data not found")
|
||||
return None
|
||||
else:
|
||||
playing_file = infolabel_path_and_file
|
||||
log.debug("get_playing_data : getPlayingFile() : {0}".format(playing_file))
|
||||
if server in playing_file:
|
||||
if item_id is not None and item_id in playing_file:
|
||||
return play_data
|
||||
elif item_id is not None and item_id not in playing_file and playing_file in playlist_data:
|
||||
# if the current now_playing data isn't correct, pull it from the playlist_data
|
||||
play_data = playlist_data.pop(playing_file)
|
||||
# Update now_playing data
|
||||
home_window.set_property('now_playing', json.dumps(play_data))
|
||||
home_window.set_property('playlist', json.dumps(playlist_data))
|
||||
return play_data
|
||||
|
||||
return play_data_map.get(playing_file)
|
||||
return {}
|
||||
|
||||
|
||||
class Service(xbmc.Player):
|
||||
|
||||
def __init__(self, *args):
|
||||
log.debug("Starting monitor service: {0}", args)
|
||||
log.debug("Starting monitor service: {0}".format(args))
|
||||
self.played_information = {}
|
||||
|
||||
def onPlayBackStarted(self):
|
||||
@@ -1124,7 +1106,7 @@ class Service(xbmc.Player):
|
||||
log.debug("onPlayBackStarted: not playing file!")
|
||||
return
|
||||
|
||||
play_data = get_playing_data(self.played_information)
|
||||
play_data = get_playing_data()
|
||||
|
||||
if play_data is None:
|
||||
return
|
||||
@@ -1141,6 +1123,7 @@ class Service(xbmc.Player):
|
||||
if jellyfin_item_id is None:
|
||||
return
|
||||
|
||||
self.played_information[jellyfin_item_id] = play_data
|
||||
log.debug("Sending Playback Started")
|
||||
postdata = {
|
||||
'QueueableMediaTypes': "Video",
|
||||
@@ -1151,7 +1134,7 @@ class Service(xbmc.Player):
|
||||
'PlaySessionId': play_session_id
|
||||
}
|
||||
|
||||
log.debug("Sending POST play started: {0}", postdata)
|
||||
log.debug("Sending POST play started: {0}".format(postdata))
|
||||
|
||||
url = "{server}/Sessions/Playing"
|
||||
download_utils.download_url(url, post_body=postdata, method="POST")
|
||||
@@ -1173,26 +1156,26 @@ class Service(xbmc.Player):
|
||||
# Will be called when kodi pauses the video
|
||||
log.debug("onPlayBackPaused")
|
||||
|
||||
play_data = get_playing_data(self.played_information)
|
||||
play_data = get_playing_data()
|
||||
|
||||
if play_data is not None:
|
||||
play_data['paused'] = True
|
||||
send_progress(self)
|
||||
send_progress()
|
||||
|
||||
def onPlayBackResumed(self):
|
||||
# Will be called when kodi resumes the video
|
||||
log.debug("onPlayBackResumed")
|
||||
|
||||
play_data = get_playing_data(self.played_information)
|
||||
play_data = get_playing_data()
|
||||
|
||||
if play_data is not None:
|
||||
play_data['paused'] = False
|
||||
send_progress(self)
|
||||
send_progress()
|
||||
|
||||
def onPlayBackSeek(self, time, seek_offset):
|
||||
# Will be called when kodi seeks in video
|
||||
log.debug("onPlayBackSeek")
|
||||
send_progress(self)
|
||||
send_progress()
|
||||
|
||||
|
||||
class PlaybackService(xbmc.Monitor):
|
||||
@@ -1202,17 +1185,18 @@ class PlaybackService(xbmc.Monitor):
|
||||
self.monitor = monitor
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
log.debug("PlaybackService:onNotification:{0}:{1}:{2}", sender, method, data)
|
||||
|
||||
if method == 'GUI.OnScreensaverActivated':
|
||||
self.screensaver_activated()
|
||||
return
|
||||
|
||||
if method == 'GUI.OnScreensaverDeactivated':
|
||||
elif method == 'GUI.OnScreensaverDeactivated':
|
||||
self.screensaver_deactivated()
|
||||
return
|
||||
elif method == 'System.OnQuit':
|
||||
home_window = HomeWindow()
|
||||
home_window.set_property('exit', 'True')
|
||||
return
|
||||
|
||||
if sender[-7:] != '.SIGNAL':
|
||||
if sender != 'plugin.video.jellycon':
|
||||
return
|
||||
|
||||
signal = method.split('.', 1)[-1]
|
||||
@@ -1220,21 +1204,17 @@ class PlaybackService(xbmc.Monitor):
|
||||
return
|
||||
|
||||
data_json = json.loads(data)
|
||||
message_data = data_json[0]
|
||||
log.debug("PlaybackService:onNotification:{0}", message_data)
|
||||
decoded_data = base64.b64decode(message_data)
|
||||
play_info = json.loads(decoded_data)
|
||||
play_info = data_json[0]
|
||||
log.debug("PlaybackService:onNotification:{0}".format(play_info))
|
||||
|
||||
if signal == "jellycon_play_action":
|
||||
log.info("Received jellycon_play_action : {0}", play_info)
|
||||
play_file(play_info, self.monitor)
|
||||
play_file(play_info)
|
||||
elif signal == "jellycon_play_youtube_trailer_action":
|
||||
log.info("Received jellycon_play_trailer_action : {0}", play_info)
|
||||
trailer_link = play_info["url"]
|
||||
xbmc.executebuiltin(trailer_link)
|
||||
elif signal == "set_view":
|
||||
view_id = play_info["view_id"]
|
||||
log.debug("Setting view id: {0}", view_id)
|
||||
log.debug("Setting view id: {0}".format(view_id))
|
||||
xbmc.executebuiltin("Container.SetViewMode(%s)" % int(view_id))
|
||||
|
||||
def screensaver_activated(self):
|
||||
@@ -1250,13 +1230,11 @@ class PlaybackService(xbmc.Monitor):
|
||||
player = xbmc.Player()
|
||||
if player.isPlayingVideo():
|
||||
log.debug("Screen Saver Activated : isPlayingVideo() = true")
|
||||
play_data = get_playing_data(self.monitor.played_information)
|
||||
play_data = get_playing_data()
|
||||
if play_data:
|
||||
log.debug("Screen Saver Activated : this is an JellyCon item so stop it")
|
||||
player.stop()
|
||||
|
||||
# xbmc.executebuiltin("Dialog.Close(selectdialog, true)")
|
||||
|
||||
clear_old_cache_data()
|
||||
|
||||
cache_images = settings.getSetting('cacheImagesOnScreenSaver') == 'true'
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
|
||||
@@ -5,10 +8,11 @@ import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .play_utils import send_event_notification
|
||||
from .kodi_utils import HomeWindow
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class PlayNextService(threading.Thread):
|
||||
@@ -30,6 +34,8 @@ class PlayNextService(threading.Thread):
|
||||
play_next_triggered = False
|
||||
is_playing = False
|
||||
|
||||
now_playing = None
|
||||
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
|
||||
player = xbmc.Player()
|
||||
@@ -38,7 +44,14 @@ class PlayNextService(threading.Thread):
|
||||
if not is_playing:
|
||||
settings = xbmcaddon.Addon()
|
||||
play_next_trigger_time = int(settings.getSetting('play_next_trigger_time'))
|
||||
log.debug("New play_next_trigger_time value: {0}", play_next_trigger_time)
|
||||
log.debug("New play_next_trigger_time value: {0}".format(play_next_trigger_time))
|
||||
|
||||
now_playing_file = player.getPlayingFile()
|
||||
if now_playing_file != now_playing:
|
||||
# If the playing file has changed, reset the play next values
|
||||
play_next_dialog = None
|
||||
play_next_triggered = False
|
||||
now_playing = now_playing_file
|
||||
|
||||
duration = player.getTotalTime()
|
||||
position = player.getTime()
|
||||
@@ -47,10 +60,10 @@ class PlayNextService(threading.Thread):
|
||||
|
||||
if not play_next_triggered and (trigger_time > time_to_end) and play_next_dialog is None:
|
||||
play_next_triggered = True
|
||||
log.debug("play_next_triggered hit at {0} seconds from end", time_to_end)
|
||||
log.debug("play_next_triggered hit at {0} seconds from end".format(time_to_end))
|
||||
|
||||
play_data = get_playing_data(self.monitor.played_information)
|
||||
log.debug("play_next_triggered play_data : {0}", play_data)
|
||||
play_data = get_playing_data()
|
||||
log.debug("play_next_triggered play_data : {0}".format(play_data))
|
||||
|
||||
next_episode = play_data.get("next_episode")
|
||||
item_type = play_data.get("item_type")
|
||||
@@ -76,6 +89,7 @@ class PlayNextService(threading.Thread):
|
||||
play_next_dialog = None
|
||||
|
||||
is_playing = False
|
||||
now_playing = None
|
||||
|
||||
if xbmc.Monitor().waitForAbort(1):
|
||||
break
|
||||
@@ -116,7 +130,7 @@ class PlayNextDialog(xbmcgui.WindowXMLDialog):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("PlayNextDialog: onMessage: {0}", message)
|
||||
log.debug("PlayNextDialog: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
@@ -125,7 +139,7 @@ class PlayNextDialog(xbmcgui.WindowXMLDialog):
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
else:
|
||||
log.debug("PlayNextDialog: onAction: {0}", action.getId())
|
||||
log.debug("PlayNextDialog: onAction: {0}".format(action.getId()))
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3013:
|
||||
@@ -133,7 +147,7 @@ class PlayNextDialog(xbmcgui.WindowXMLDialog):
|
||||
self.play_called
|
||||
self.close()
|
||||
next_item_id = self.episode_info.get("Id")
|
||||
log.debug("Playing Next Episode: {0}", next_item_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"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .translation import string_load
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ResumeDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
|
||||
@@ -36,7 +37,7 @@ class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("SafeDeleteDialog: onMessage: {0}", message)
|
||||
log.debug("SafeDeleteDialog: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
@@ -45,7 +46,7 @@ class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
else:
|
||||
log.debug("SafeDeleteDialog: onAction: {0}", action.getId())
|
||||
log.debug("SafeDeleteDialog: onAction: {0}".format(action.getId()))
|
||||
|
||||
def onClick(self, controlID):
|
||||
if controlID == 1:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import socket
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
import httplib
|
||||
from six.moves.urllib.parse import urlparse
|
||||
import requests
|
||||
import ssl
|
||||
import time
|
||||
import hashlib
|
||||
@@ -12,15 +13,17 @@ from datetime import datetime
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
from six import ensure_binary
|
||||
from kodi_six.utils import py2_decode
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .downloadutils import DownloadUtils, save_user_details, load_user_details
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .translation import string_load
|
||||
from .utils import datetime_from_string
|
||||
from .clientinfo import ClientInformation
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
__addon__ = xbmcaddon.Addon()
|
||||
__addon_name__ = __addon__.getAddonInfo('name')
|
||||
@@ -40,66 +43,45 @@ def check_connection_speed():
|
||||
|
||||
url = server + "/playback/bitratetest?size=%s" % test_data_size
|
||||
|
||||
url_bits = urlparse(url.strip())
|
||||
|
||||
protocol = url_bits.scheme
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
# user_name = url_bits.username
|
||||
# user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_puery = url_bits.query
|
||||
|
||||
server = "%s:%s" % (host_name, port)
|
||||
url_path = url_path + "?" + url_puery
|
||||
|
||||
local_use_https = False
|
||||
if protocol.lower() == "https":
|
||||
local_use_https = True
|
||||
|
||||
if local_use_https and verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout)
|
||||
elif local_use_https and not verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert NOT checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout, context=ssl._create_unverified_context())
|
||||
else:
|
||||
log.debug("Connection: HTTP")
|
||||
conn = httplib.HTTPConnection(server, timeout=http_timeout)
|
||||
|
||||
head = du.get_auth_header(True)
|
||||
head["User-Agent"] = "JellyCon-" + ClientInformation().get_version()
|
||||
|
||||
conn.request(method="GET", url=url_path, headers=head)
|
||||
request_details = {
|
||||
"stream": True,
|
||||
"headers": head
|
||||
}
|
||||
|
||||
if not verify_cert:
|
||||
request_details["verify"] = False
|
||||
|
||||
progress_dialog = xbmcgui.DialogProgress()
|
||||
message = 'Testing with {0} MB of data'.format(speed_test_data_size)
|
||||
progress_dialog.create("JellyCon connection speed test", message)
|
||||
total_data_read = 0
|
||||
total_time = time.time()
|
||||
start_time = time.time()
|
||||
|
||||
log.debug("Starting Connection Speed Test")
|
||||
response = conn.getresponse()
|
||||
|
||||
response = requests.get(url, **request_details)
|
||||
|
||||
last_percentage_done = 0
|
||||
if int(response.status) == 200:
|
||||
data = response.read(10240)
|
||||
while len(data) > 0:
|
||||
total_data_read = 0
|
||||
if response.status_code == 200:
|
||||
for data in response.iter_content(chunk_size=10240):
|
||||
total_data_read += len(data)
|
||||
percentage_done = int(float(total_data_read) / float(test_data_size) * 100.0)
|
||||
if last_percentage_done != percentage_done:
|
||||
progress_dialog.update(percentage_done)
|
||||
last_percentage_done = percentage_done
|
||||
data = response.read(10240)
|
||||
else:
|
||||
log.error("HTTP response error: {0} {1}", response.status, response.reason)
|
||||
error_message = "HTTP response error: %s\n%s" % (response.status, response.reason)
|
||||
log.error("HTTP response error: {0} {1}".format(response.status_code, response.content))
|
||||
error_message = "HTTP response error: %s\n%s" % (response.status_code, response.content)
|
||||
xbmcgui.Dialog().ok("Speed Test Error", error_message)
|
||||
return -1
|
||||
|
||||
total_data_read_kbits = (total_data_read * 8) / 1000
|
||||
total_time = time.time() - total_time
|
||||
total_time = time.time() - start_time
|
||||
speed = int(total_data_read_kbits / total_time)
|
||||
log.debug("Finished Connection Speed Test, speed: {0} total_data: {1}, total_time: {2}", speed, total_data_read, total_time)
|
||||
log.debug("Finished Connection Speed Test, speed: {0} total_data: {1}, total_time: {2}".format(speed, total_data_read, total_time))
|
||||
|
||||
progress_dialog.close()
|
||||
del progress_dialog
|
||||
@@ -119,10 +101,9 @@ def check_safe_delete_available():
|
||||
log.debug("check_safe_delete_available")
|
||||
|
||||
du = DownloadUtils()
|
||||
json_data = du.download_url("{server}/Plugins")
|
||||
result = json.loads(json_data)
|
||||
if result is not None:
|
||||
log.debug("Server Plugin List: {0}", result)
|
||||
result = du.download_url("{server}/Plugins")
|
||||
if result:
|
||||
log.debug("Server Plugin List: {0}".format(result))
|
||||
|
||||
safe_delete_found = False
|
||||
for plugin in result:
|
||||
@@ -130,7 +111,7 @@ def check_safe_delete_available():
|
||||
safe_delete_found = True
|
||||
break
|
||||
|
||||
log.debug("Safe Delete Plugin Available: {0}", safe_delete_found)
|
||||
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")
|
||||
@@ -145,23 +126,20 @@ def get_server_details():
|
||||
log.debug("Getting Server Details from Network")
|
||||
servers = []
|
||||
|
||||
message = "who is JellyfinServer?"
|
||||
message = b"who is JellyfinServer?"
|
||||
multi_group = ("<broadcast>", 7359)
|
||||
# multi_group = ("127.0.0.1", 7359)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(4.0)
|
||||
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 3) # timeout
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
|
||||
|
||||
log.debug("MutliGroup: {0}", multi_group)
|
||||
log.debug("Sending UDP Data: {0}", message)
|
||||
log.debug("MutliGroup: {0}".format(multi_group))
|
||||
log.debug("Sending UDP Data: {0}".format(message))
|
||||
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(__addon_name__ + " : " + string_load(30373))
|
||||
progress.create('{} : {}'.format(__addon_name__, string_load(30373)))
|
||||
progress.update(0, string_load(30374))
|
||||
xbmc.sleep(1000)
|
||||
server_count = 0
|
||||
@@ -172,18 +150,18 @@ def get_server_details():
|
||||
while True:
|
||||
try:
|
||||
server_count += 1
|
||||
progress.update(server_count * 10, string_load(30375) % server_count)
|
||||
progress.update(server_count * 10, '{}: {}'.format(string_load(30375), server_count))
|
||||
xbmc.sleep(1000)
|
||||
data, addr = sock.recvfrom(1024)
|
||||
servers.append(json.loads(data))
|
||||
except:
|
||||
break
|
||||
except Exception as e:
|
||||
log.error("UPD Discovery Error: {0}", e)
|
||||
log.error("UPD Discovery Error: {0}".format(e))
|
||||
|
||||
progress.close()
|
||||
|
||||
log.debug("Found Servers: {0}", servers)
|
||||
log.debug("Found Servers: {0}".format(servers))
|
||||
return servers
|
||||
|
||||
|
||||
@@ -221,14 +199,14 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
server_list.append(server_item)
|
||||
|
||||
if len(server_list) > 0:
|
||||
return_index = xbmcgui.Dialog().select(__addon_name__ + " : " + string_load(30166),
|
||||
return_index = xbmcgui.Dialog().select('{} : {}'.format(__addon_name__, string_load(30166)),
|
||||
server_list,
|
||||
useDetails=True)
|
||||
if return_index != -1:
|
||||
server_url = server_info[return_index]["Address"]
|
||||
|
||||
if not server_url:
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__, string_load(30282), string_load(30370))
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__, '{}\n{}'.format(string_load(30282), string_load(30370)))
|
||||
if not return_index:
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
return
|
||||
@@ -247,68 +225,35 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
return
|
||||
|
||||
url_bits = urlparse(server_url)
|
||||
server_address = url_bits.hostname
|
||||
server_port = str(url_bits.port)
|
||||
server_protocol = url_bits.scheme
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
public_lookup_url = "%s/System/Info/Public?format=json" % (server_url)
|
||||
|
||||
if user_name and user_password:
|
||||
temp_url = "%s://%s:%s@%s:%s/Users/Public?format=json" % (server_protocol, user_name, user_password, server_address, server_port)
|
||||
else:
|
||||
temp_url = "%s://%s:%s/Users/Public?format=json" % (server_protocol, server_address, server_port)
|
||||
|
||||
log.debug("Testing_Url: {0}", temp_url)
|
||||
log.debug("Testing_Url: {0}".format(public_lookup_url))
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(__addon_name__ + " : " + string_load(30376))
|
||||
progress.create('{} : {}'.format(__addon_name__, string_load(30376)))
|
||||
progress.update(0, string_load(30377))
|
||||
json_data = du.download_url(temp_url, authenticate=False)
|
||||
result = du.download_url(public_lookup_url, authenticate=False)
|
||||
progress.close()
|
||||
|
||||
result = json.loads(json_data)
|
||||
if result is not None:
|
||||
xbmcgui.Dialog().ok(__addon_name__ + " : " + string_load(30167),
|
||||
"%s://%s:%s/" % (server_protocol, server_address, server_port))
|
||||
if result:
|
||||
xbmcgui.Dialog().ok('{} : {}'.format(__addon_name__, string_load(30167)),
|
||||
server_url)
|
||||
break
|
||||
else:
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__ + " : " + string_load(30135),
|
||||
return_index = xbmcgui.Dialog().yesno('{} : {}'.format(__addon_name__, string_load(30135)),
|
||||
server_url,
|
||||
string_load(30371))
|
||||
if not return_index:
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
return
|
||||
|
||||
log.debug("Selected server: {0}", server_url)
|
||||
|
||||
# parse the url
|
||||
url_bits = urlparse(server_url)
|
||||
server_address = url_bits.hostname
|
||||
server_port = str(url_bits.port)
|
||||
server_protocol = url_bits.scheme
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
log.debug("Detected server info {0} - {1} - {2}", server_protocol, server_address, server_port)
|
||||
|
||||
# save the server info
|
||||
settings.setSetting("port", server_port)
|
||||
|
||||
if user_name and user_password:
|
||||
server_address = "%s:%s@%s" % (url_bits.username, url_bits.password, server_address)
|
||||
|
||||
settings.setSetting("ipaddress", server_address)
|
||||
|
||||
if server_protocol == "https":
|
||||
settings.setSetting("protocol", "1")
|
||||
else:
|
||||
settings.setSetting("protocol", "0")
|
||||
|
||||
log.debug("Selected server: {0}".format(server_url))
|
||||
settings.setSetting("server_address", server_url)
|
||||
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 = py2_decode(current_username)
|
||||
|
||||
# if asked or we have no current user then show user selection screen
|
||||
if something_changed or change_user or len(current_username) == 0:
|
||||
@@ -319,168 +264,158 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
|
||||
# get a list of users
|
||||
log.debug("Getting user list")
|
||||
json_data = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
|
||||
result = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
|
||||
|
||||
log.debug("jsonData: {0}", json_data)
|
||||
try:
|
||||
result = json.loads(json_data)
|
||||
except:
|
||||
result = None
|
||||
log.debug("jsonData: {0}".format(py2_decode(result)))
|
||||
|
||||
if result is None:
|
||||
xbmcgui.Dialog().ok(string_load(30135),
|
||||
string_load(30201),
|
||||
string_load(30169) + server_url)
|
||||
selected_id = -1
|
||||
users = []
|
||||
for user in result:
|
||||
config = user.get("Configuration")
|
||||
if config is not None:
|
||||
if config.get("IsHidden", False) is False:
|
||||
name = user.get("Name")
|
||||
admin = user.get("Policy", {}).get("IsAdministrator", False)
|
||||
|
||||
time_ago = ""
|
||||
last_active = user.get("LastActivityDate")
|
||||
if last_active:
|
||||
last_active_date = datetime_from_string(last_active)
|
||||
log.debug("LastActivityDate: {0}".format(last_active_date))
|
||||
ago = datetime.now() - last_active_date
|
||||
log.debug("LastActivityDate: {0}".format(ago))
|
||||
days = divmod(ago.seconds, 86400)
|
||||
hours = divmod(days[1], 3600)
|
||||
minutes = divmod(hours[1], 60)
|
||||
log.debug("LastActivityDate: {0} {1} {2}".format(days[0], hours[0], minutes[0]))
|
||||
if days[0]:
|
||||
time_ago += " %sd" % days[0]
|
||||
if hours[0]:
|
||||
time_ago += " %sh" % hours[0]
|
||||
if minutes[0]:
|
||||
time_ago += " %sm" % minutes[0]
|
||||
time_ago = time_ago.strip()
|
||||
if not time_ago:
|
||||
time_ago = "Active: now"
|
||||
else:
|
||||
time_ago = "Active: %s ago" % time_ago
|
||||
log.debug("LastActivityDate: {0}".format(time_ago))
|
||||
|
||||
user_item = xbmcgui.ListItem(name)
|
||||
user_image = du.get_user_artwork(user, 'Primary')
|
||||
if not user_image:
|
||||
user_image = "DefaultUser.png"
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2("TEST")
|
||||
|
||||
sub_line = time_ago
|
||||
|
||||
if user.get("HasPassword", False) is True:
|
||||
sub_line += ", Password"
|
||||
user_item.setProperty("secure", "true")
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(ensure_binary(name))
|
||||
hashed_username = m.hexdigest()
|
||||
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
|
||||
if saved_password:
|
||||
sub_line += ": Saved"
|
||||
|
||||
else:
|
||||
user_item.setProperty("secure", "false")
|
||||
|
||||
if admin:
|
||||
sub_line += ", Admin"
|
||||
else:
|
||||
sub_line += ", User"
|
||||
|
||||
user_item.setProperty("manual", "false")
|
||||
user_item.setLabel2(sub_line)
|
||||
users.append(user_item)
|
||||
|
||||
if current_username == name:
|
||||
selected_id = len(users) - 1
|
||||
|
||||
if current_username:
|
||||
selection_title = string_load(30180) + " (" + current_username + ")"
|
||||
else:
|
||||
selected_id = -1
|
||||
users = []
|
||||
for user in result:
|
||||
config = user.get("Configuration")
|
||||
if config is not None:
|
||||
if config.get("IsHidden", False) is False:
|
||||
name = user.get("Name")
|
||||
admin = user.get("Policy", {}).get("IsAdministrator", False)
|
||||
selection_title = string_load(30180)
|
||||
|
||||
time_ago = ""
|
||||
last_active = user.get("LastActivityDate")
|
||||
if last_active:
|
||||
last_active_date = datetime_from_string(last_active)
|
||||
log.debug("LastActivityDate: {0}", last_active_date)
|
||||
ago = datetime.now() - last_active_date
|
||||
log.debug("LastActivityDate: {0}", ago)
|
||||
days = divmod(ago.seconds, 86400)
|
||||
hours = divmod(days[1], 3600)
|
||||
minutes = divmod(hours[1], 60)
|
||||
log.debug("LastActivityDate: {0} {1} {2}", days[0], hours[0], minutes[0])
|
||||
if days[0]:
|
||||
time_ago += " %sd" % days[0]
|
||||
if hours[0]:
|
||||
time_ago += " %sh" % hours[0]
|
||||
if minutes[0]:
|
||||
time_ago += " %sm" % minutes[0]
|
||||
time_ago = time_ago.strip()
|
||||
if not time_ago:
|
||||
time_ago = "Active: now"
|
||||
else:
|
||||
time_ago = "Active: %s ago" % time_ago
|
||||
log.debug("LastActivityDate: {0}", time_ago)
|
||||
# add manual login
|
||||
user_item = xbmcgui.ListItem(string_load(30365))
|
||||
art = {"Thumb": "DefaultUser.png"}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2(string_load(30366))
|
||||
user_item.setProperty("secure", "true")
|
||||
user_item.setProperty("manual", "true")
|
||||
users.append(user_item)
|
||||
|
||||
user_item = xbmcgui.ListItem(name)
|
||||
user_image = du.get_user_artwork(user, 'Primary')
|
||||
if not user_image:
|
||||
user_image = "DefaultUser.png"
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2("TEST")
|
||||
return_value = xbmcgui.Dialog().select(selection_title,
|
||||
users,
|
||||
preselect=selected_id,
|
||||
autoclose=20000,
|
||||
useDetails=True)
|
||||
|
||||
sub_line = time_ago
|
||||
if return_value > -1 and return_value != selected_id:
|
||||
|
||||
if user.get("HasPassword", False) is True:
|
||||
sub_line += ", Password"
|
||||
user_item.setProperty("secure", "true")
|
||||
something_changed = True
|
||||
selected_user = users[return_value]
|
||||
secured = selected_user.getProperty("secure") == "true"
|
||||
manual = selected_user.getProperty("manual") == "true"
|
||||
selected_user_name = selected_user.getLabel()
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(name)
|
||||
hashed_username = m.hexdigest()
|
||||
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
|
||||
if saved_password:
|
||||
sub_line += ": Saved"
|
||||
log.debug("Selected User Name: {0} : {1}".format(return_value, selected_user_name))
|
||||
|
||||
else:
|
||||
user_item.setProperty("secure", "false")
|
||||
if manual:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30005))
|
||||
if current_username:
|
||||
kb.setDefault(current_username)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
selected_user_name = kb.getText()
|
||||
log.debug("Manual entered username: {0}".format(selected_user_name))
|
||||
else:
|
||||
return
|
||||
|
||||
if admin:
|
||||
sub_line += ", Admin"
|
||||
else:
|
||||
sub_line += ", User"
|
||||
if secured:
|
||||
# we need a password, check the settings first
|
||||
m = hashlib.md5()
|
||||
m.update(selected_user_name.encode())
|
||||
hashed_username = m.hexdigest()
|
||||
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
|
||||
allow_password_saving = settings.getSetting("allow_password_saving") == "true"
|
||||
|
||||
user_item.setProperty("manual", "false")
|
||||
user_item.setLabel2(sub_line)
|
||||
users.append(user_item)
|
||||
# if not saving passwords but have a saved ask to clear it
|
||||
if not allow_password_saving and saved_password:
|
||||
clear_password = xbmcgui.Dialog().yesno(string_load(30368), string_load(30369))
|
||||
if clear_password:
|
||||
settings.setSetting("saved_user_password_" + hashed_username, "")
|
||||
|
||||
if current_username == name:
|
||||
selected_id = len(users) - 1
|
||||
if saved_password:
|
||||
log.debug("Saving username and password: {0}".format(selected_user_name))
|
||||
log.debug("Using stored password for user: {0}".format(hashed_username))
|
||||
save_user_details(settings, selected_user_name, saved_password)
|
||||
|
||||
if current_username:
|
||||
selection_title = string_load(30180) + " (" + current_username + ")"
|
||||
else:
|
||||
selection_title = string_load(30180)
|
||||
|
||||
# add manual login
|
||||
user_item = xbmcgui.ListItem(string_load(30365))
|
||||
art = {"Thumb": "DefaultUser.png"}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2(string_load(30366))
|
||||
user_item.setProperty("secure", "true")
|
||||
user_item.setProperty("manual", "true")
|
||||
users.append(user_item)
|
||||
|
||||
return_value = xbmcgui.Dialog().select(selection_title,
|
||||
users,
|
||||
preselect=selected_id,
|
||||
autoclose=20000,
|
||||
useDetails=True)
|
||||
|
||||
if return_value > -1 and return_value != selected_id:
|
||||
|
||||
something_changed = True
|
||||
selected_user = users[return_value]
|
||||
secured = selected_user.getProperty("secure") == "true"
|
||||
manual = selected_user.getProperty("manual") == "true"
|
||||
selected_user_name = selected_user.getLabel()
|
||||
|
||||
log.debug("Selected User Name: {0} : {1}", return_value, selected_user_name)
|
||||
|
||||
if manual:
|
||||
else:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30005))
|
||||
if current_username:
|
||||
kb.setDefault(current_username)
|
||||
kb.setHeading(string_load(30006))
|
||||
kb.setHiddenInput(True)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
selected_user_name = kb.getText()
|
||||
log.debug("Manual entered username: {0}", selected_user_name)
|
||||
else:
|
||||
return
|
||||
log.debug("Saving username and password: {0}".format(selected_user_name))
|
||||
save_user_details(settings, selected_user_name, kb.getText())
|
||||
|
||||
if secured:
|
||||
# we need a password, check the settings first
|
||||
m = hashlib.md5()
|
||||
m.update(selected_user_name)
|
||||
hashed_username = m.hexdigest()
|
||||
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
|
||||
allow_password_saving = settings.getSetting("allow_password_saving") == "true"
|
||||
|
||||
# if not saving passwords but have a saved ask to clear it
|
||||
if not allow_password_saving and saved_password:
|
||||
clear_password = xbmcgui.Dialog().yesno(string_load(30368), string_load(30369))
|
||||
if clear_password:
|
||||
settings.setSetting("saved_user_password_" + hashed_username, "")
|
||||
|
||||
if saved_password:
|
||||
log.debug("Saving username and password: {0}", selected_user_name)
|
||||
log.debug("Using stored password for user: {0}", hashed_username)
|
||||
save_user_details(settings, selected_user_name, saved_password)
|
||||
|
||||
else:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30006))
|
||||
kb.setHiddenInput(True)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
log.debug("Saving username and password: {0}", selected_user_name)
|
||||
save_user_details(settings, selected_user_name, kb.getText())
|
||||
|
||||
# should we save the password
|
||||
if allow_password_saving:
|
||||
save_password = xbmcgui.Dialog().yesno(string_load(30363), string_load(30364))
|
||||
if save_password:
|
||||
log.debug("Saving password for fast user switching: {0}", hashed_username)
|
||||
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
|
||||
else:
|
||||
log.debug("Saving username with no password: {0}", selected_user_name)
|
||||
save_user_details(settings, selected_user_name, "")
|
||||
# should we save the password
|
||||
if allow_password_saving:
|
||||
save_password = xbmcgui.Dialog().yesno(string_load(30363), string_load(30364))
|
||||
if save_password:
|
||||
log.debug("Saving password for fast user switching: {0}".format(hashed_username))
|
||||
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
|
||||
else:
|
||||
log.debug("Saving username with no password: {0}".format(selected_user_name))
|
||||
save_user_details(settings, selected_user_name, "")
|
||||
|
||||
if something_changed:
|
||||
home_window = HomeWindow()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import sys
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .utils import get_art
|
||||
from .datamanager import DataManager
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def show_server_sessions():
|
||||
@@ -29,7 +29,7 @@ def show_server_sessions():
|
||||
|
||||
url = "{server}/Sessions"
|
||||
results = data_manager.get_content(url)
|
||||
log.debug("session_info: {0}", results)
|
||||
log.debug("session_info: {0}".format(results))
|
||||
|
||||
if results is None:
|
||||
return
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
from .jsonrpc import JsonRpc
|
||||
|
||||
|
||||
class SimpleLogging:
|
||||
name = ""
|
||||
enable_logging = False
|
||||
|
||||
def __init__(self, name):
|
||||
settings = xbmcaddon.Addon()
|
||||
prefix = settings.getAddonInfo('name')
|
||||
self.name = prefix + '.' + name
|
||||
self.enable_logging = settings.getSetting('log_debug') == "true"
|
||||
|
||||
# params = {"setting": "debug.showloginfo"}
|
||||
# setting_result = json_rpc('Settings.getSettingValue').execute(params)
|
||||
# current_value = setting_result.get("result", None)
|
||||
# if current_value is not None:
|
||||
# self.enable_logging = current_value.get("value", False)
|
||||
# xbmc.log("LOGGING_ENABLED %s : %s" % (self.name, str(self.enable_logging)), level=xbmc.LOGDEBUG)
|
||||
|
||||
def __str__(self):
|
||||
return "LoggingEnabled: " + str(self.enable_logging)
|
||||
|
||||
def info(self, fmt, *args, **kwargs):
|
||||
log_line = self.name + "|INFO|" + self.log_line(fmt, *args)
|
||||
xbmc.log(log_line, level=xbmc.LOGNOTICE)
|
||||
|
||||
def error(self, fmt, *args, **kwargs):
|
||||
log_line = self.name + "|ERROR|" + self.log_line(fmt, *args)
|
||||
xbmc.log(log_line, level=xbmc.LOGERROR)
|
||||
|
||||
def debug(self, fmt, *args, **kwargs):
|
||||
if self.enable_logging:
|
||||
log_line = self.name + "|DEBUG|" + self.log_line(fmt, *args)
|
||||
xbmc.log(log_line, level=xbmc.LOGNOTICE)
|
||||
|
||||
@staticmethod
|
||||
def log_line(fmt, *args):
|
||||
new_args = []
|
||||
# convert any unicode to utf-8 strings
|
||||
for arg in args:
|
||||
if isinstance(arg, unicode):
|
||||
new_args.append(arg.encode("utf-8"))
|
||||
else:
|
||||
new_args.append(arg)
|
||||
log_line = fmt.format(*new_args)
|
||||
return log_line
|
||||
@@ -1,15 +1,17 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
|
||||
from .jsonrpc import JsonRpc, get_value, set_value
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
ver = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
||||
|
||||
|
||||
@@ -27,9 +29,6 @@ def clone_default_skin():
|
||||
set_skin_settings()
|
||||
update_kodi_settings()
|
||||
|
||||
# xbmc.executebuiltin("ReloadSkin()")
|
||||
# xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
|
||||
|
||||
def walk_path(root_path, relative_path, all_files):
|
||||
files = xbmcvfs.listdir(root_path)
|
||||
@@ -52,7 +51,7 @@ def clone_skin():
|
||||
|
||||
kodi_path = xbmc.translatePath("special://xbmc")
|
||||
kodi_skin_source = os.path.join(kodi_path, "addons", "skin.estuary")
|
||||
log.debug("Kodi Skin Source: {0}", kodi_skin_source)
|
||||
log.debug("Kodi Skin Source: {0}".format(kodi_skin_source))
|
||||
|
||||
pdialog = xbmcgui.DialogProgress()
|
||||
pdialog.create("JellyCon Skin Cloner", "")
|
||||
@@ -60,11 +59,11 @@ def clone_skin():
|
||||
all_files = []
|
||||
walk_path(kodi_skin_source, "", all_files)
|
||||
for found in all_files:
|
||||
log.debug("Found Path: {0}", found)
|
||||
log.debug("Found Path: {0}".format(found))
|
||||
|
||||
kodi_home_path = xbmc.translatePath("special://home")
|
||||
kodi_skin_destination = os.path.join(kodi_home_path, "addons", "skin.estuary_jellycon")
|
||||
log.debug("Kodi Skin Destination: {0}", kodi_skin_destination)
|
||||
log.debug("Kodi Skin Destination: {0}".format(kodi_skin_destination))
|
||||
|
||||
# copy all skin files (clone)
|
||||
count = 0
|
||||
@@ -81,22 +80,18 @@ def clone_skin():
|
||||
|
||||
# alter skin addon.xml
|
||||
addon_xml_path = os.path.join(kodi_skin_destination, "addon.xml")
|
||||
with open(addon_xml_path, "r") as addon_file:
|
||||
addon_xml_data = addon_file.read()
|
||||
addon_tree = ET.parse(addon_xml_path)
|
||||
addon_root = addon_tree.getroot()
|
||||
|
||||
addon_xml_data = addon_xml_data.replace("id=\"skin.estuary\"", "id=\"skin.estuary_jellycon\"")
|
||||
addon_xml_data = addon_xml_data.replace("name=\"Estuary\"", "name=\"Estuary JellyCon\"")
|
||||
addon_root.attrib['id'] = 'skin.estuary_jellycon'
|
||||
addon_root.attrib['name'] = 'Estuary JellyCon'
|
||||
|
||||
# log.debug("{0}", addon_xml_data)
|
||||
|
||||
# update the addon.xml
|
||||
with open(addon_xml_path, "w") as addon_file:
|
||||
addon_file.write(addon_xml_data)
|
||||
addon_tree.write(addon_xml_path)
|
||||
|
||||
# get jellycon path
|
||||
jellycon_path = os.path.join(kodi_home_path, "addons", "plugin.video.jellycon")
|
||||
|
||||
log.debug("Major Version: {0}", ver)
|
||||
log.debug("Major Version: {0}".format(ver))
|
||||
|
||||
file_list = ["Home.xml",
|
||||
"Includes_Home.xml",
|
||||
@@ -104,6 +99,7 @@ def clone_skin():
|
||||
"DialogSeekBar.xml",
|
||||
"VideoOSD.xml"]
|
||||
|
||||
# Copy customized skin files from our addon into cloned skin
|
||||
for file_name in file_list:
|
||||
source = os.path.join(jellycon_path, "resources", "skins", "skin.estuary", ver, "xml", file_name)
|
||||
destination = os.path.join(kodi_skin_destination, "xml", file_name)
|
||||
@@ -123,11 +119,11 @@ def clone_skin():
|
||||
'enabled': True
|
||||
}
|
||||
result = JsonRpc('Addons.SetAddonEnabled').execute(params)
|
||||
log.debug("Addons.SetAddonEnabled : {0}", result)
|
||||
log.debug("Addons.SetAddonEnabled : {0}".format(result))
|
||||
|
||||
log.debug("SkinCloner : Current Skin : " + get_value("lookandfeel.skin"))
|
||||
set_result = set_value("lookandfeel.skin", "skin.estuary_jellycon")
|
||||
log.debug("Save Setting : lookandfeel.skin : {0}", set_result)
|
||||
log.debug("Save Setting : lookandfeel.skin : {0}".format(set_result))
|
||||
log.debug("SkinCloner : Current Skin : " + get_value("lookandfeel.skin"))
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
import functools
|
||||
import time
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
enabled = False
|
||||
|
||||
@@ -27,6 +28,6 @@ def timer(func):
|
||||
data = args[1]
|
||||
elif func.__name__ == "main_entry_point" and len(sys.argv) > 2:
|
||||
data = sys.argv[2]
|
||||
log.info("timing_data|{0}|{1}|{2}|{3}", func.__name__, started, ended, data)
|
||||
log.info("timing_data|{0}|{1}|{2}|{3}".format(func.__name__, started, ended, data))
|
||||
return value
|
||||
return wrapper
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote, unquote
|
||||
import encodings
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .datamanager import DataManager
|
||||
|
||||
from .translation import string_load
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
dataManager = DataManager()
|
||||
|
||||
details_string = 'EpisodeCount,SeasonCount,Path,Etag,MediaStreams'
|
||||
@@ -19,11 +20,11 @@ icon = xbmc.translatePath('special://home/addons/plugin.video.jellycon/icon.png'
|
||||
|
||||
|
||||
def not_found(content_string):
|
||||
xbmcgui.Dialog().notification('JellyCon', string_load(30305) % content_string, icon=icon, sound=False)
|
||||
xbmcgui.Dialog().notification('JellyCon', '{}: {}'.format(string_load(30305), content_string), icon=icon, sound=False)
|
||||
|
||||
|
||||
def playback_starting(content_string):
|
||||
xbmcgui.Dialog().notification('JellyCon', string_load(30306) % content_string, icon=icon, sound=False)
|
||||
xbmcgui.Dialog().notification('JellyCon', '{}: {}'.format(string_load(30306), content_string), icon=icon, sound=False)
|
||||
|
||||
|
||||
def search(item_type, query):
|
||||
@@ -105,13 +106,13 @@ def get_episode_id(parent_id, episode):
|
||||
|
||||
|
||||
def get_match(item_type, title, year, imdb_id):
|
||||
query = urllib.quote(title)
|
||||
query = quote(title)
|
||||
|
||||
results = search(item_type, query=query)
|
||||
results = results.get('SearchHints')
|
||||
if results is None:
|
||||
results = []
|
||||
log.debug('SearchHints jsonData: {0}', results)
|
||||
log.debug('SearchHints jsonData: {0}'.format(results))
|
||||
|
||||
potential_matches = []
|
||||
|
||||
@@ -121,12 +122,12 @@ def get_match(item_type, title, year, imdb_id):
|
||||
if (name == title and int(year) == production_year) or (int(year) == production_year):
|
||||
potential_matches.append(item)
|
||||
|
||||
log.debug('Potential matches: {0}', potential_matches)
|
||||
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}', item)
|
||||
log.debug('Found match: {0}'.format(item))
|
||||
return item
|
||||
|
||||
return None
|
||||
@@ -137,7 +138,7 @@ def entry_point(parameters):
|
||||
action = parameters.get('action', None)
|
||||
video_type = parameters.get('video_type', None)
|
||||
|
||||
title = urllib.unquote(parameters.get('title', ''))
|
||||
title = unquote(parameters.get('title', ''))
|
||||
|
||||
year = parameters.get('year', '')
|
||||
episode = parameters.get('episode', '')
|
||||
@@ -245,4 +246,4 @@ def entry_point(parameters):
|
||||
not_found('{title} ({year}) - S{season}'.format(title=title, year=year, season=str_season))
|
||||
|
||||
if url and media_type:
|
||||
xbmc.executebuiltin('ActivateWindow(Videos, plugin://plugin.video.jellycon/?mode=GET_CONTENT&url={url}&media_type={media_type})'.format(url=urllib.quote(url), media_type=media_type))
|
||||
xbmc.executebuiltin('ActivateWindow(Videos, plugin://plugin.video.jellycon/?mode=GET_CONTENT&url={url}&media_type={media_type})'.format(url=quote(url), media_type=media_type))
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import xbmcaddon
|
||||
from .simple_logging import SimpleLogging
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
import xbmcaddon
|
||||
from .loghandler import LazyLogger
|
||||
from kodi_six.utils import py2_encode
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
addon = xbmcaddon.Addon()
|
||||
|
||||
|
||||
def string_load(string_id):
|
||||
try:
|
||||
return addon.getLocalizedString(string_id).encode('utf-8', 'ignore')
|
||||
return py2_encode(addon.getLocalizedString(string_id))
|
||||
except Exception as e:
|
||||
log.error('Failed String Load: {0} ({1})', string_id, e)
|
||||
return str(string_id)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
|
||||
import binascii
|
||||
import string
|
||||
import random
|
||||
import urllib
|
||||
import json
|
||||
import base64
|
||||
import time
|
||||
@@ -13,9 +15,11 @@ import math
|
||||
from datetime import datetime
|
||||
import calendar
|
||||
import re
|
||||
from six import ensure_text, ensure_binary
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .clientinfo import ClientInformation
|
||||
|
||||
# hack to get datetime strptime loaded
|
||||
@@ -23,22 +27,16 @@ throwaway = time.strptime('20110101', '%Y%m%d')
|
||||
|
||||
# define our global download utils
|
||||
downloadUtils = DownloadUtils()
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def get_jellyfin_url(base_url, params):
|
||||
params["format"] = "json"
|
||||
param_list = []
|
||||
for key in params:
|
||||
if params[key] is not None:
|
||||
value = params[key]
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode("utf8")
|
||||
else:
|
||||
value = str(value)
|
||||
param_list.append(key + "=" + urllib.quote_plus(value, safe="{}"))
|
||||
param_string = "&".join(param_list)
|
||||
return base_url + "?" + param_string
|
||||
url_params = urlencode(params)
|
||||
# Filthy hack until I get around to reworking the network flow
|
||||
# It relies on {thing} strings in downloadutils.py
|
||||
url_params = url_params.replace('%7B', '{').replace('%7D', '}')
|
||||
return base_url + "?" + url_params
|
||||
|
||||
|
||||
###########################################################################
|
||||
@@ -88,14 +86,14 @@ class PlayUtils:
|
||||
if direct_path.startswith("//"):
|
||||
direct_path = "smb://" + direct_path[2:]
|
||||
|
||||
log.debug("playback_direct_path: {0}", direct_path)
|
||||
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:
|
||||
# check if file can be direct streamed/played
|
||||
if (can_direct_stream or can_direct_play) and playurl is None:
|
||||
item_id = media_source.get('Id')
|
||||
playurl = ("%s/Videos/%s/stream" +
|
||||
"?static=true" +
|
||||
@@ -137,7 +135,7 @@ class PlayUtils:
|
||||
if playback_video_force_8:
|
||||
transcode_params.update({"MaxVideoBitDepth": "8"})
|
||||
|
||||
transcode_path = urllib.urlencode(transcode_params)
|
||||
transcode_path = urlencode(transcode_params)
|
||||
|
||||
playurl = "%s/Videos/%s/master.m3u8?%s" % (server, item_id, transcode_path)
|
||||
|
||||
@@ -164,13 +162,13 @@ class PlayUtils:
|
||||
lines = contents.split(line_break)
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
log.debug("STRM Line: {0}", line)
|
||||
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}", item_property, 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")
|
||||
@@ -181,7 +179,7 @@ class PlayUtils:
|
||||
playurl = line
|
||||
log.debug("STRM playback url found")
|
||||
|
||||
log.debug("Playback URL: {0} ListItem Properties: {1}", playurl, listitem_props)
|
||||
log.debug("Playback URL: {0} ListItem Properties: {1}".format(playurl, listitem_props))
|
||||
return playurl, listitem_props
|
||||
|
||||
|
||||
@@ -218,9 +216,8 @@ def get_art(item, server):
|
||||
'tvshow.landscape': ''
|
||||
}
|
||||
|
||||
image_tags = item["ImageTags"]
|
||||
if image_tags is not None and image_tags["Primary"] is not None:
|
||||
# image_tag = image_tags["Primary"]
|
||||
image_tags = item.get("ImageTags", {})
|
||||
if image_tags and image_tags.get("Primary"):
|
||||
art['thumb'] = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
|
||||
item_type = item["Type"]
|
||||
@@ -229,7 +226,6 @@ def get_art(item, server):
|
||||
art['poster'] = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
elif item_type == "Episode":
|
||||
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=True, server=server)
|
||||
# art['poster'] = downloadUtils.getArtwork(item, "Primary", parent=True, server=server)
|
||||
art['tvshow.clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
|
||||
art['clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
|
||||
art['tvshow.clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
|
||||
@@ -288,27 +284,26 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
|
||||
def double_urlencode(text):
|
||||
text = single_urlencode(text)
|
||||
text = single_urlencode(text)
|
||||
return text
|
||||
|
||||
|
||||
def single_urlencode(text):
|
||||
# urlencode needs a utf- string
|
||||
text = urllib.urlencode({'blahblahblah': text.encode('utf-8')})
|
||||
text = urlencode({'blahblahblah': text.encode('utf-8')})
|
||||
text = text[13:]
|
||||
return text.decode('utf-8') # return the result again as unicode
|
||||
|
||||
|
||||
def send_event_notification(method, data):
|
||||
message_data = json.dumps(data)
|
||||
source_id = "jellycon"
|
||||
base64_data = base64.b64encode(message_data)
|
||||
escaped_data = '\\"[\\"{0}\\"]\\"'.format(base64_data)
|
||||
command = 'XBMC.NotifyAll({0}.SIGNAL,{1},{2})'.format(source_id, method, escaped_data)
|
||||
log.debug("Sending notification event data: {0}", 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):
|
||||
@@ -317,7 +312,7 @@ def datetime_from_string(time_string):
|
||||
time_string = re.sub("[0-9]{1}Z", " UTC", time_string)
|
||||
elif time_string[-6:] == "+00:00":
|
||||
time_string = re.sub("[0-9]{1}\+00:00", " UTC", time_string)
|
||||
log.debug("New Time String : {0}", time_string)
|
||||
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]))
|
||||
|
||||
@@ -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,21 +2,23 @@
|
||||
|
||||
#################################################################################################
|
||||
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
import json
|
||||
import threading
|
||||
import websocket
|
||||
import time
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .functions import play_action
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from . import clientinfo
|
||||
from . import downloadutils
|
||||
from .jsonrpc import JsonRpc
|
||||
from .kodi_utils import HomeWindow
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketClient(threading.Thread):
|
||||
@@ -31,6 +33,7 @@ class WebSocketClient(threading.Thread):
|
||||
|
||||
self.__dict__ = self._shared_state
|
||||
self.monitor = xbmc.Monitor()
|
||||
self.retry_count = 0
|
||||
|
||||
self.client_info = clientinfo.ClientInformation()
|
||||
self.device_id = self.client_info.get_device_id()
|
||||
@@ -65,10 +68,10 @@ class WebSocketClient(threading.Thread):
|
||||
self._general_commands(data)
|
||||
|
||||
else:
|
||||
log.debug("WebSocket Message Type: {0}", message)
|
||||
log.debug("WebSocket Message Type: {0}".format(message))
|
||||
|
||||
def _library_changed(self, data):
|
||||
log.debug("Library_Changed: {0}", data)
|
||||
log.debug("Library_Changed: {0}".format(data))
|
||||
self._library_monitor.check_for_updates()
|
||||
|
||||
def _play(self, data):
|
||||
@@ -81,7 +84,7 @@ class WebSocketClient(threading.Thread):
|
||||
home_screen.set_property("skip_select_user", "true")
|
||||
|
||||
startat = data.get('StartPositionTicks', -1)
|
||||
log.debug("WebSocket Message PlayNow: {0}", data)
|
||||
log.debug("WebSocket Message PlayNow: {0}".format(data))
|
||||
|
||||
media_source_id = data.get("MediaSourceId", "")
|
||||
subtitle_stream_index = data.get("SubtitleStreamIndex", None)
|
||||
@@ -124,14 +127,14 @@ class WebSocketClient(threading.Thread):
|
||||
seek_to = data['SeekPositionTicks']
|
||||
seek_time = seek_to / 10000000.0
|
||||
player.seekTime(seek_time)
|
||||
log.debug("Seek to {0}", seek_time)
|
||||
log.debug("Seek to {0}".format(seek_time))
|
||||
|
||||
elif command in actions:
|
||||
actions[command]()
|
||||
log.debug("Command: {0} completed", command)
|
||||
log.debug("Command: {0} completed".format(command))
|
||||
|
||||
else:
|
||||
log.debug("Unknown command: {0}", command)
|
||||
log.debug("Unknown command: {0}".format(command))
|
||||
return
|
||||
|
||||
def _general_commands(self, data):
|
||||
@@ -175,7 +178,7 @@ class WebSocketClient(threading.Thread):
|
||||
# header = arguments['Header']
|
||||
text = arguments['Text']
|
||||
# show notification here
|
||||
log.debug("WebSocket DisplayMessage: {0}", text)
|
||||
log.debug("WebSocket DisplayMessage: {0}".format(text))
|
||||
xbmcgui.Dialog().notification("JellyCon", text)
|
||||
|
||||
elif command == 'SendString':
|
||||
@@ -226,15 +229,13 @@ class WebSocketClient(threading.Thread):
|
||||
if command in builtin:
|
||||
xbmc.executebuiltin(builtin[command])
|
||||
|
||||
def on_close(self, ws):
|
||||
log.debug("Closed")
|
||||
|
||||
def on_open(self, ws):
|
||||
log.debug("Connected")
|
||||
self.retry_count = 0
|
||||
self.post_capabilities()
|
||||
|
||||
def on_error(self, ws, error):
|
||||
log.debug("Error: {0}", error)
|
||||
log.debug("Error: {0}".format(error))
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -249,23 +250,25 @@ class WebSocketClient(threading.Thread):
|
||||
|
||||
# Get the appropriate prefix for the websocket
|
||||
server = download_utils.get_server()
|
||||
if "https" in server:
|
||||
server = server.replace('https', "wss")
|
||||
if "https://" in server:
|
||||
server = server.replace('https://', 'wss://')
|
||||
else:
|
||||
server = server.replace('http', "ws")
|
||||
server = server.replace('http://', 'ws://')
|
||||
|
||||
websocket_url = "%s/websocket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
|
||||
log.debug("websocket url: {0}", websocket_url)
|
||||
websocket_url = "%s/socket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
|
||||
log.debug("websocket url: {0}".format(websocket_url))
|
||||
|
||||
self._client = websocket.WebSocketApp(
|
||||
websocket_url,
|
||||
on_open=lambda ws: self.on_open(ws),
|
||||
on_message=lambda ws, message: self.on_message(ws, message),
|
||||
on_error=lambda ws, error: self.on_error(ws, error))
|
||||
|
||||
self._client = websocket.WebSocketApp(websocket_url,
|
||||
on_open=self.on_open,
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_close=self.on_close)
|
||||
log.debug("Starting WebSocketClient")
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
|
||||
time.sleep(self.retry_count * 5)
|
||||
self._client.run_forever(ping_interval=10)
|
||||
|
||||
if self._stop_websocket:
|
||||
@@ -275,6 +278,8 @@ class WebSocketClient(threading.Thread):
|
||||
# Abort was requested, exit
|
||||
break
|
||||
|
||||
if self.retry_count < 12:
|
||||
self.retry_count += 1
|
||||
log.debug("Reconnecting WebSocket")
|
||||
|
||||
log.debug("WebSocketClient Stopped")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
import json
|
||||
import hashlib
|
||||
import random
|
||||
import time
|
||||
@@ -10,12 +11,12 @@ import time
|
||||
from .downloadutils import DownloadUtils
|
||||
from .utils import get_jellyfin_url
|
||||
from .datamanager import DataManager
|
||||
from .simple_logging import SimpleLogging
|
||||
from .loghandler import LazyLogger
|
||||
from .kodi_utils import HomeWindow
|
||||
from .dir_functions import process_directory
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
downloadUtils = DownloadUtils()
|
||||
dataManager = DataManager()
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
@@ -43,7 +44,6 @@ def set_random_movies():
|
||||
url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
|
||||
|
||||
results = downloadUtils.download_url(url, suppress=True)
|
||||
results = json.loads(results)
|
||||
|
||||
randon_movies_list = []
|
||||
if results is not None:
|
||||
@@ -55,17 +55,17 @@ def set_random_movies():
|
||||
movies_list_string = ",".join(randon_movies_list)
|
||||
home_window = HomeWindow()
|
||||
m = hashlib.md5()
|
||||
m.update(movies_list_string)
|
||||
m.update(movies_list_string.encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
|
||||
log.debug("set_random_movies : {0}", movies_list_string)
|
||||
log.debug("set_random_movies : {0}", new_widget_hash)
|
||||
log.debug("set_random_movies : {0}".format(movies_list_string))
|
||||
log.debug("set_random_movies : {0}".format(new_widget_hash))
|
||||
home_window.set_property("random-movies", movies_list_string)
|
||||
home_window.set_property("random-movies-changed", new_widget_hash)
|
||||
|
||||
|
||||
def set_background_image(force=False):
|
||||
log.debug("set_background_image Called forced={0}", force)
|
||||
log.debug("set_background_image Called forced={0}".format(force))
|
||||
|
||||
global background_current_item
|
||||
global background_items
|
||||
@@ -76,12 +76,12 @@ def set_background_image(force=False):
|
||||
background_items = []
|
||||
|
||||
if len(background_items) == 0:
|
||||
log.debug("set_background_image: Need to load more backgrounds {0} - {1}",
|
||||
len(background_items), background_current_item)
|
||||
log.debug("set_background_image: Need to load more backgrounds {0} - {1}".format(
|
||||
len(background_items), background_current_item))
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
# url_params["limit"] = 60
|
||||
url_params["limit"] = 100
|
||||
url_params["SortBy"] = "Random"
|
||||
url_params["IncludeItemTypes"] = "Movie,Series"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
@@ -90,14 +90,14 @@ def set_background_image(force=False):
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
results = downloadUtils.download_url(url, suppress=True)
|
||||
results = json.loads(results)
|
||||
|
||||
if results is not None:
|
||||
items = results.get("Items", [])
|
||||
background_current_item = 0
|
||||
background_items = []
|
||||
for item in items:
|
||||
bg_image = downloadUtils.get_artwork(item, "Backdrop", server=server)
|
||||
bg_image = downloadUtils.get_artwork(
|
||||
item, "Backdrop", server=server)
|
||||
if bg_image:
|
||||
label = item.get("Name")
|
||||
item_background = {}
|
||||
@@ -105,12 +105,14 @@ def set_background_image(force=False):
|
||||
item_background["name"] = label
|
||||
background_items.append(item_background)
|
||||
|
||||
log.debug("set_background_image: Loaded {0} more backgrounds", len(background_items))
|
||||
log.debug("set_background_image: Loaded {0} more backgrounds".format(
|
||||
len(background_items)))
|
||||
|
||||
if len(background_items) > 0:
|
||||
bg_image = background_items[background_current_item].get("image")
|
||||
label = background_items[background_current_item].get("name")
|
||||
log.debug("set_background_image: {0} - {1} - {2}", background_current_item, label, bg_image)
|
||||
log.debug(
|
||||
"set_background_image: {0} - {1} - {2}".format(background_current_item, label, bg_image))
|
||||
|
||||
background_current_item += 1
|
||||
if background_current_item >= len(background_items):
|
||||
@@ -127,13 +129,14 @@ def check_for_new_content():
|
||||
|
||||
home_window = HomeWindow()
|
||||
settings = xbmcaddon.Addon()
|
||||
simple_new_content_check = settings.getSetting("simple_new_content_check") == "true"
|
||||
simple_new_content_check = settings.getSetting(
|
||||
"simple_new_content_check") == "true"
|
||||
|
||||
if simple_new_content_check:
|
||||
log.debug("Using simple new content check")
|
||||
current_time_stamp = str(time.time())
|
||||
home_window.set_property("jellycon_widget_reload", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
|
||||
return
|
||||
|
||||
url_params = {}
|
||||
@@ -144,13 +147,11 @@ def check_for_new_content():
|
||||
url_params["SortOrder"] = "Descending"
|
||||
url_params["IncludeItemTypes"] = "Movie,Episode"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
url_params["format"] = "json"
|
||||
|
||||
added_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
|
||||
added_result = downloadUtils.download_url(added_url, suppress=True)
|
||||
result = json.loads(added_result)
|
||||
log.debug("LATEST_ADDED_ITEM: {0}", result)
|
||||
result = downloadUtils.download_url(added_url, suppress=True)
|
||||
log.debug("LATEST_ADDED_ITEM: {0}".format(result))
|
||||
|
||||
last_added_date = ""
|
||||
if result is not None:
|
||||
@@ -158,7 +159,7 @@ def check_for_new_content():
|
||||
if len(items) > 0:
|
||||
item = items[0]
|
||||
last_added_date = item.get("Etag", "")
|
||||
log.debug("last_added_date: {0}", last_added_date)
|
||||
log.debug("last_added_date: {0}".format(last_added_date))
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
@@ -168,48 +169,46 @@ def check_for_new_content():
|
||||
url_params["SortOrder"] = "Descending"
|
||||
url_params["IncludeItemTypes"] = "Movie,Episode"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
url_params["format"] = "json"
|
||||
|
||||
played_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
|
||||
played_result = downloadUtils.download_url(played_url, suppress=True)
|
||||
result = json.loads(played_result)
|
||||
log.debug("LATEST_PLAYED_ITEM: {0}", result)
|
||||
result = downloadUtils.download_url(played_url, suppress=True)
|
||||
log.debug("LATEST_PLAYED_ITEM: {0}".format(result))
|
||||
|
||||
last_played_date = ""
|
||||
if result is not None:
|
||||
items = result.get("Items", [])
|
||||
if len(items) > 0:
|
||||
item = items[0]
|
||||
# last_played_date = item.get("Etag", "")
|
||||
user_data = item.get("UserData", None)
|
||||
if user_data is not None:
|
||||
last_played_date = user_data.get("LastPlayedDate", "")
|
||||
|
||||
log.debug("last_played_date: {0}", last_played_date)
|
||||
log.debug("last_played_date: {0}".format(last_played_date))
|
||||
|
||||
current_widget_hash = home_window.get_property("jellycon_widget_reload")
|
||||
log.debug("Current Widget Hash: {0}", current_widget_hash)
|
||||
log.debug("Current Widget Hash: {0}".format(current_widget_hash))
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(last_played_date + last_added_date)
|
||||
new_widget_hash = m.hexdigest()
|
||||
log.debug("New Widget Hash: {0}", new_widget_hash)
|
||||
log.debug("New Widget Hash: {0}".format(new_widget_hash))
|
||||
|
||||
if current_widget_hash != new_widget_hash:
|
||||
home_window.set_property("jellycon_widget_reload", new_widget_hash)
|
||||
log.debug("Setting New Widget Hash: {0}", new_widget_hash)
|
||||
log.debug("Setting New Widget Hash: {0}".format(new_widget_hash))
|
||||
|
||||
|
||||
@timer
|
||||
def get_widget_content_cast(handle, params):
|
||||
log.debug("getWigetContentCast Called: {0}", params)
|
||||
log.debug("getWigetContentCast Called: {0}".format(params))
|
||||
server = downloadUtils.get_server()
|
||||
|
||||
item_id = params["id"]
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content("{server}/Users/{userid}/Items/" + item_id + "?format=json")
|
||||
log.debug("ItemInfo: {0}", result)
|
||||
result = data_manager.get_content(
|
||||
"{server}/Users/{userid}/Items/" + item_id)
|
||||
log.debug("ItemInfo: {0}".format(result))
|
||||
|
||||
if not result:
|
||||
return
|
||||
@@ -227,12 +226,6 @@ def get_widget_content_cast(handle, params):
|
||||
people = []
|
||||
|
||||
for person in people:
|
||||
# if (person.get("Type") == "Director"):
|
||||
# director = director + person.get("Name") + ' '
|
||||
# if (person.get("Type") == "Writing"):
|
||||
# writer = person.get("Name")
|
||||
# if (person.get("Type") == "Writer"):
|
||||
# writer = person.get("Name")
|
||||
if person.get("Type") == "Actor":
|
||||
person_name = person.get("Name")
|
||||
person_role = person.get("Role")
|
||||
@@ -240,7 +233,8 @@ def get_widget_content_cast(handle, params):
|
||||
person_tag = person.get("PrimaryImageTag")
|
||||
person_thumbnail = None
|
||||
if person_tag:
|
||||
person_thumbnail = downloadUtils.image_url(person_id, "Primary", 0, 400, 400, person_tag, server=server)
|
||||
person_thumbnail = downloadUtils.image_url(
|
||||
person_id, "Primary", 0, 400, 400, person_tag, server=server)
|
||||
|
||||
if kodi_version > 17:
|
||||
list_item = xbmcgui.ListItem(label=person_name, offscreen=True)
|
||||
@@ -272,26 +266,27 @@ def get_widget_content_cast(handle, params):
|
||||
|
||||
@timer
|
||||
def get_widget_content(handle, params):
|
||||
log.debug("getWigetContent Called: {0}", params)
|
||||
log.debug("getWigetContent Called: {0}".format(params))
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
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
|
||||
|
||||
log.debug("widget_type: {0}", widget_type)
|
||||
log.debug("widget_type: {0}".format(widget_type))
|
||||
|
||||
url_verb = "{server}/Users/{userid}/Items"
|
||||
url_params = {}
|
||||
url_params["Limit"] = "{ItemLimit}"
|
||||
url_params["format"] = "json"
|
||||
url_params["Fields"] = "{field_filters}"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
url_params["IsMissing"] = False
|
||||
in_progress = False
|
||||
|
||||
if widget_type == "recent_movies":
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
@@ -303,6 +298,7 @@ def get_widget_content(handle, params):
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Movie"
|
||||
url_params["Limit"] = 20
|
||||
|
||||
elif widget_type == "inprogress_movies":
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
@@ -312,6 +308,7 @@ def get_widget_content(handle, params):
|
||||
url_params["Filters"] = "IsResumable"
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Movie"
|
||||
url_params["Limit"] = 20
|
||||
|
||||
elif widget_type == "random_movies":
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
@@ -331,7 +328,7 @@ def get_widget_content(handle, params):
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
url_params["format"] = "json"
|
||||
url_params["Limit"] = 20
|
||||
|
||||
elif widget_type == "recent_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
@@ -343,6 +340,7 @@ def get_widget_content(handle, params):
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode"
|
||||
url_params["Limit"] = 20
|
||||
|
||||
elif widget_type == "inprogress_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
@@ -352,16 +350,27 @@ def get_widget_content(handle, params):
|
||||
url_params["Filters"] = "IsResumable"
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode"
|
||||
url_params["Limit"] = 20
|
||||
|
||||
elif widget_type == "nextup_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
url_verb = "{server}/Shows/NextUp"
|
||||
url_params = url_params.copy()
|
||||
url_params["Limit"] = "{ItemLimit}"
|
||||
url_params["userid"] = "{userid}"
|
||||
url_params["Recursive"] = True
|
||||
url_params["Fields"] = "{field_filters}"
|
||||
url_params["format"] = "json"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
# Collect InProgress items to be combined with NextUp
|
||||
inprogress_url_verb = "{server}/Users/{userid}/Items"
|
||||
inprogress_url_params = url_params.copy()
|
||||
inprogress_url_params["Recursive"] = True
|
||||
inprogress_url_params["SortBy"] = "DatePlayed"
|
||||
inprogress_url_params["SortOrder"] = "Descending"
|
||||
inprogress_url_params["Filters"] = "IsResumable"
|
||||
inprogress_url_params["IsVirtualUnaired"] = False
|
||||
inprogress_url_params["IncludeItemTypes"] = "Episode"
|
||||
inprogress_url_params["Limit"] = 20
|
||||
in_progress = True
|
||||
|
||||
elif widget_type == "movie_recommendations":
|
||||
suggested_items_url_params = {}
|
||||
@@ -369,7 +378,8 @@ def get_widget_content(handle, params):
|
||||
suggested_items_url_params["categoryLimit"] = 15
|
||||
suggested_items_url_params["ItemLimit"] = 20
|
||||
suggested_items_url_params["ImageTypeLimit"] = 0
|
||||
suggested_items_url = get_jellyfin_url("{server}/Movies/Recommendations", suggested_items_url_params)
|
||||
suggested_items_url = get_jellyfin_url(
|
||||
"{server}/Movies/Recommendations", suggested_items_url_params)
|
||||
|
||||
data_manager = DataManager()
|
||||
suggested_items = data_manager.get_content(suggested_items_url)
|
||||
@@ -377,47 +387,42 @@ def get_widget_content(handle, params):
|
||||
set_id = 0
|
||||
while len(ids) < 20 and suggested_items:
|
||||
items = suggested_items[set_id]
|
||||
log.debug("BaselineItemName : {0} - {1}", set_id, items.get("BaselineItemName"))
|
||||
log.debug(
|
||||
"BaselineItemName : {0} - {1}".format(set_id, items.get("BaselineItemName")))
|
||||
items = items["Items"]
|
||||
rand = random.randint(0, len(items) - 1)
|
||||
# log.debug("random suggestions index : {0} {1}", rand, set_id)
|
||||
item = items[rand]
|
||||
if item["Type"] == "Movie" and item["Id"] not in ids and (not item["UserData"]["Played"] or not hide_watched):
|
||||
# log.debug("random suggestions adding : {0}", item["Id"])
|
||||
ids.append(item["Id"])
|
||||
# else:
|
||||
# log.debug("random suggestions not valid : {0} - {1} - {2}", item["Id"], item["Type"], item["UserData"]["Played"])
|
||||
del items[rand]
|
||||
# log.debug("items len {0}", len(items))
|
||||
if len(items) == 0:
|
||||
# log.debug("Removing Set {0}", set_id)
|
||||
del suggested_items[set_id]
|
||||
set_id += 1
|
||||
if set_id >= len(suggested_items):
|
||||
set_id = 0
|
||||
|
||||
id_list = ",".join(ids)
|
||||
log.debug("Recommended Items : {0}", len(ids), id_list)
|
||||
log.debug("Recommended Items : {0}".format(len(ids), id_list))
|
||||
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)
|
||||
list_items, detected_type, total_records = process_directory(
|
||||
items_url, None, params, use_cached_widget_data)
|
||||
|
||||
# remove resumable items from next up
|
||||
# Combine In Progress and Next Up Episodes, apend next up after In Progress
|
||||
if widget_type == "nextup_episodes":
|
||||
filtered_list = []
|
||||
for item in list_items:
|
||||
resume_time = item[1].getProperty("ResumeTime")
|
||||
if resume_time is None or float(resume_time) == 0.0:
|
||||
filtered_list.append(item)
|
||||
list_items = filtered_list
|
||||
inprogress_url = get_jellyfin_url(
|
||||
inprogress_url_verb, inprogress_url_params)
|
||||
|
||||
# list_items = populateWidgetItems(items_url, widget_type)
|
||||
list_items_inprogress, detected_type, total_records = process_directory(
|
||||
inprogress_url, None, params, use_cached_widget_data)
|
||||
|
||||
list_items = list_items_inprogress + list_items
|
||||
|
||||
if detected_type is not None:
|
||||
# if the media type is not set then try to use the detected type
|
||||
log.debug("Detected content type: {0}", detected_type)
|
||||
log.debug("Detected content type: {0}".format(detected_type))
|
||||
content_type = None
|
||||
|
||||
if detected_type == "Movie":
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
<setting label="30388" type="lsep"/>
|
||||
<setting label="30011" type="action" action="RunScript(plugin.video.jellycon,0,?mode=DETECT_SERVER_USER)" option="close"/>
|
||||
<setting id="protocol" type="select" label="30390" lvalues="30391|30392" default="0" />
|
||||
<setting id="ipaddress" type="text" label="30000" default="<none>" visible="true" enable="true" />
|
||||
<setting id="port" type="text" label="30001" default="8096" visible="true" enable="true" />
|
||||
<setting id="ipaddress" type="text" label="30000" default="" visible="false" enable="false" />
|
||||
<setting id="protocol" type="select" label="30390" lvalues="30391|30392" default="0" visible="false"/>
|
||||
<setting id="port" type="text" label="30001" default="8096" visible="false" enable="false" />
|
||||
<setting id="server_address" type="text" label="30000" default="" visible="true" enable="true" />
|
||||
<setting id="verify_cert" type="bool" label="30003" default="false" visible="true" enable="true" />
|
||||
|
||||
<setting label="30389" type="lsep"/>
|
||||
@@ -45,13 +46,6 @@
|
||||
<setting id="audio_playback_bitrate" type="select" label="30418" values="128|160|192|256|320|384|448|640" default="256" visible="true"/>
|
||||
<setting id="audio_max_channels" type="slider" label="30420" default="8" range="2,1,8" option="int" visible="true"/>
|
||||
|
||||
<!--
|
||||
<setting label="30209" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="smbusername" type="text" label="30007" default="" enable="true" visible="true"/>
|
||||
<setting id="smbpassword" type="text" label="30008" default="" option="hidden" enable="true" visible="true"/>
|
||||
-->
|
||||
|
||||
</category>
|
||||
<category label="30214">
|
||||
|
||||
@@ -73,8 +67,8 @@
|
||||
|
||||
<setting label="30329" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="stopPlaybackOnScreensaver" type="bool" label="30332" default="true" visible="true" enable="true" />
|
||||
<setting id="changeUserOnScreenSaver" type="bool" label="30330" default="true" visible="true" enable="true" />
|
||||
<setting id="stopPlaybackOnScreensaver" type="bool" label="30332" default="false" visible="true" enable="true" />
|
||||
<setting id="changeUserOnScreenSaver" type="bool" label="30330" default="false" visible="true" enable="true" />
|
||||
<setting id="cacheImagesOnScreenSaver" type="bool" label="30333" default="true" visible="true" enable="true" />
|
||||
<setting id="cacheImagesOnScreenSaver_interval" type="slider" label="30400" default="0" range="0,1,60" option="int" visible="true"/>
|
||||
|
||||
@@ -133,7 +127,7 @@
|
||||
<setting id="use_cached_widget_data" type="bool" label="30441" default="false" visible="true" enable="true" />
|
||||
<setting id="showLoadProgress" type="bool" label="30120" default="false" visible="true" enable="true" />
|
||||
<setting id="suppressErrors" type="bool" label="30315" default="false" visible="true" enable="true" />
|
||||
<setting id="speed_test_data_size" type="slider" label="30436" default="15" range="5,1,100" option="int" visible="true"/>
|
||||
<setting id="speed_test_data_size" type="slider" label="30436" default="10" range="1,1,10" option="int" visible="true"/>
|
||||
|
||||
</category>
|
||||
<category label="30421">
|
||||
@@ -154,4 +148,4 @@
|
||||
<setting id="sort-Episodes" type="select" label="30235" lvalues="30423|30424|30426|30425|30427|30429|30430|30428" default="0" visible="true"/>
|
||||
|
||||
</category>
|
||||
</settings>
|
||||
</settings>
|
||||
|
||||
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 137 B |
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 109 B |
26
service.py
@@ -9,7 +9,7 @@ import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from resources.lib.downloadutils import DownloadUtils, save_user_details
|
||||
from resources.lib.simple_logging import SimpleLogging
|
||||
from resources.lib.loghandler 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
|
||||
@@ -35,13 +35,13 @@ home_window.clear_property("userid")
|
||||
home_window.clear_property("AccessToken")
|
||||
home_window.clear_property("Params")
|
||||
|
||||
log = SimpleLogging('service')
|
||||
log = LazyLogger('service')
|
||||
monitor = xbmc.Monitor()
|
||||
|
||||
try:
|
||||
clear_old_cache_data()
|
||||
except Exception as error:
|
||||
log.error("Error in clear_old_cache_data() : {0}", error)
|
||||
log.error("Error in clear_old_cache_data() : {0}".format(error))
|
||||
|
||||
# wait for 10 seconds for the Kodi splash screen to close
|
||||
i = 0
|
||||
@@ -60,7 +60,7 @@ try:
|
||||
download_utils.authenticate()
|
||||
download_utils.get_user_id()
|
||||
except Exception as error:
|
||||
log.error("Error with initial service auth: {0}", error)
|
||||
log.error("Error with initial service auth: {0}".format(error))
|
||||
|
||||
|
||||
image_server = HttpImageServerThread()
|
||||
@@ -112,13 +112,11 @@ if enable_logging:
|
||||
time=8000,
|
||||
icon=xbmcgui.NOTIFICATION_WARNING)
|
||||
|
||||
# monitor.abortRequested() is causes issues, it currently triggers for all addon cancelations which causes
|
||||
# the service to exit when a user cancels an addon load action. This is a bug in Kodi.
|
||||
# I am switching back to xbmc.abortRequested approach until kodi is fixed or I find a work arround
|
||||
prev_user_id = home_window.get_property("userid")
|
||||
first_run = True
|
||||
home_window.set_property('exit', 'False')
|
||||
|
||||
while not xbmc.abortRequested:
|
||||
while home_window.get_property('exit') == 'False':
|
||||
|
||||
try:
|
||||
if xbmc.Player().isPlaying():
|
||||
@@ -126,7 +124,7 @@ while not xbmc.abortRequested:
|
||||
# if playing every 10 seconds updated the server with progress
|
||||
if (time.time() - last_progress_update) > 10:
|
||||
last_progress_update = time.time()
|
||||
send_progress(monitor)
|
||||
send_progress()
|
||||
|
||||
else:
|
||||
screen_saver_active = xbmc.getCondVisibility("System.ScreenSaverActive")
|
||||
@@ -181,14 +179,17 @@ while not xbmc.abortRequested:
|
||||
set_background_image(False)
|
||||
|
||||
except Exception as error:
|
||||
log.error("Exception in Playback Monitor: {0}", error)
|
||||
log.error("{0}", traceback.format_exc())
|
||||
log.error("Exception in Playback Monitor: {0}".format(error))
|
||||
log.error("{0}".format(traceback.format_exc()))
|
||||
|
||||
first_run = False
|
||||
xbmc.sleep(1000)
|
||||
|
||||
image_server.stop()
|
||||
|
||||
# stop the WebSocket Client
|
||||
websocket_client.stop_client()
|
||||
|
||||
# call stop on the library update monitor
|
||||
library_change_monitor.stop()
|
||||
|
||||
@@ -200,9 +201,6 @@ if play_next_service:
|
||||
if context_monitor:
|
||||
context_monitor.stop_monitor()
|
||||
|
||||
# stop the WebSocket Client
|
||||
websocket_client.stop_client()
|
||||
|
||||
# clear user and token when loggin off
|
||||
home_window.clear_property("userid")
|
||||
home_window.clear_property("AccessToken")
|
||||
|
||||