Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
22
.ci/azure-pipelines.yml
Normal file
22
.ci/azure-pipelines.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
steps:
|
||||
# On every PR, build the addon and make it available for download as an artifact
|
||||
- template: build.yml
|
||||
parameters:
|
||||
py_versions: [ 'py2', 'py3' ]
|
||||
|
||||
# When triggered by a tag, publish the built addon to the repo server
|
||||
- ${{ if startsWith(variables['Build.SourceBranch'], 'refs/tags') }}:
|
||||
- template: publish.yml
|
||||
parameters:
|
||||
py_versions: [ 'py2', 'py3' ]
|
||||
45
.ci/build.yml
Normal file
45
.ci/build.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
parameters:
|
||||
python_versions : []
|
||||
|
||||
steps:
|
||||
- ${{ each py_version in parameters.py_versions }}:
|
||||
- task: usePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.6'
|
||||
|
||||
- checkout: self
|
||||
clean: true
|
||||
|
||||
- script: python3 -m pip install --user -r jellycon/requirements-dev.txt
|
||||
displayName: 'Install dev dependencies'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Create clean addon directory'
|
||||
inputs:
|
||||
sourceFolder: 'jellycon'
|
||||
cleanTargetFolder: true
|
||||
contents: |
|
||||
**/*
|
||||
!.ci/*
|
||||
!.git/**/*
|
||||
!.github/*
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/plugin.video.jellycon'
|
||||
|
||||
- script: python3 '$(Build.ArtifactStagingDirectory)/plugin.video.jellycon/build.py' --version ${{ py_version }} --target '$(Build.ArtifactStagingDirectory)/'
|
||||
displayName: 'Create ${{ py_version }} addon.xml'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Create ${{ py_version }} zip file'
|
||||
inputs:
|
||||
rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/plugin.video.jellycon'
|
||||
includeRootFolder: True
|
||||
archiveType: 'zip'
|
||||
tarCompression: 'none'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/plugin.video.jellycon-${{ py_version }}.zip'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish ${{ py_version }} artifact'
|
||||
inputs:
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)/plugin.video.jellycon'
|
||||
artifactName: 'plugin.video.jellycon-${{ py_version }}-$(Build.BuildNumber)'
|
||||
|
||||
27
.ci/publish.yml
Normal file
27
.ci/publish.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
parameters:
|
||||
python_version : []
|
||||
|
||||
steps:
|
||||
- ${{ each py_version in parameters.py_versions }}:
|
||||
- task: CopyFilesOverSSH@0
|
||||
displayName: 'Upload to repo server'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
sourceFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
contents: 'plugin.video.jellycon-${{ py_version }}.zip'
|
||||
targetFolder: '/srv/repository/incoming/kodi'
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Add to Kodi repo'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: 'python3 /usr/local/bin/kodirepo add /srv/repository/incoming/kodi/plugin.video.jellycon-${{ py_version }}.zip --datadir /srv/repository/releases/client/kodi/${{ py_version }}'
|
||||
failOnStdErr: false
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Clean up zip files'
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: 'rm /srv/repository/incoming/kodi/plugin.video.jellycon-${{ py_version }}.zip'
|
||||
70
.config/generate_xml.py
Normal file
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,14 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.jellycon"
|
||||
name="JellyCon"
|
||||
version="0.3.1"
|
||||
version=""
|
||||
provider-name="Jellyfin Contributors">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.25.0"/>
|
||||
<import addon="script.module.pil" version="1.1.7"/>
|
||||
<import addon="script.module.requests" version="2.22.0"/>
|
||||
<import addon="script.module.six" version="1.13.0"/>
|
||||
<import addon="script.module.kodi-six" />
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||
<provides>video audio</provides>
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -220,3 +220,6 @@ pip-log.txt
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
# Addon files
|
||||
addon.xml
|
||||
|
||||
108
build.py
Normal file
108
build.py
Normal 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.jellyfin+{}.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.jellyfin' not in filename and 'pyo' not in filename:
|
||||
file_path = os.path.join(root, filename)
|
||||
relative_path = os.path.join('plugin.video.jellyfin', 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)
|
||||
36
release.yaml
Normal file
36
release.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
version: '0.4.2'
|
||||
changelog: |
|
||||
- #60 - Remove multicast socket options from autodiscovery
|
||||
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'
|
||||
1
requirements-dev.txt
Normal file
1
requirements-dev.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyyaml
|
||||
@@ -219,7 +219,7 @@ msgid "Include people"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error: %s"
|
||||
msgid "URL error"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30201"
|
||||
@@ -555,11 +555,11 @@ msgid "Cached Jellyfin images : "
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30305"
|
||||
msgid "Not Found: %s"
|
||||
msgid "Not Found"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30306"
|
||||
msgid "Playback starting: %s"
|
||||
msgid "Playback starting"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30307"
|
||||
@@ -683,7 +683,7 @@ msgid "Changes Require Kodi Restart"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30344"
|
||||
msgid "Number of images removed from cache : %s"
|
||||
msgid "Number of images removed from cache"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30345"
|
||||
@@ -815,7 +815,7 @@ msgid "Sending request"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30375"
|
||||
msgid "Receiving data packet: %s"
|
||||
msgid "Receiving data packet"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30376"
|
||||
@@ -891,7 +891,7 @@ msgid "Clear Cache Result"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30394"
|
||||
msgid "%s cache files deleted"
|
||||
msgid "Cache files deleted"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30395"
|
||||
|
||||
@@ -26,7 +26,7 @@ class ActionAutoClose(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
log.debug("ActionAutoClose Running")
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
time_since_last = time.time() - self.last_interaction
|
||||
log.debug("ActionAutoClose time_since_last : {0}".format(time_since_last))
|
||||
|
||||
@@ -70,9 +70,6 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
self.listControl.addItems(self.action_items)
|
||||
self.setFocus(self.listControl)
|
||||
|
||||
# bg_image = self.getControl(3010)
|
||||
# bg_image.setHeight(50 * len(self.action_items) + 20)
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
|
||||
@@ -41,8 +41,6 @@ class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
# log.debug("onAction: onAction: {0} {1}", action.getId(), self.slider_control.getInt())
|
||||
|
||||
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
|
||||
self.bitrate_label.setLabel(bitrate_label_string)
|
||||
|
||||
@@ -57,4 +55,3 @@ class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3000:
|
||||
log.debug("ActionMenu: Selected Item: {0}".format(control_id))
|
||||
#self.close()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import unquote
|
||||
import requests
|
||||
import base64
|
||||
import sys
|
||||
@@ -107,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")
|
||||
@@ -119,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
|
||||
|
||||
@@ -150,11 +150,10 @@ class CacheArtwork(threading.Thread):
|
||||
unused_texture_ids = set()
|
||||
for texture in textures:
|
||||
url = texture.get("url")
|
||||
url = urllib.unquote(url)
|
||||
url = unquote(url)
|
||||
url = url.replace("image://", "")
|
||||
url = url[0:-1]
|
||||
if url.find("/") > -1 and url not in jellyfin_texture_urls or url.find("localhost:24276") > -1:
|
||||
# log.debug("adding unused texture url: {0}", url)
|
||||
unused_texture_ids.add(texture["textureid"])
|
||||
|
||||
total = len(unused_texture_ids)
|
||||
@@ -243,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:
|
||||
@@ -285,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)
|
||||
@@ -306,7 +304,6 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
missing_texture_urls = set()
|
||||
|
||||
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
|
||||
for image_url in jellyfin_texture_urls:
|
||||
if image_url not in texture_urls and not image_url.endswith("&Tag=") and len(image_url) > 0:
|
||||
missing_texture_urls.add(image_url)
|
||||
@@ -329,7 +326,6 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
count_done = 0
|
||||
for index, get_url in enumerate(missing_texture_urls, 1):
|
||||
# log.debug("texture_url: {0}", get_url)
|
||||
url = double_urlencode(get_url)
|
||||
kodi_texture_url = ("/image/image://%s" % url)
|
||||
log.debug("kodi_texture_url: {0}".format(kodi_texture_url))
|
||||
@@ -345,8 +341,6 @@ class CacheArtwork(threading.Thread):
|
||||
count_done += 1
|
||||
log.debug("Get Image Result: {0}".format(data.status_code))
|
||||
|
||||
# if progress.iscanceled():
|
||||
# if "iscanceled" in dir(progress) and progress.iscanceled():
|
||||
if isinstance(progress, xbmcgui.DialogProgress) and progress.iscanceled():
|
||||
break
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
from uuid import uuid4 as uuid4
|
||||
from uuid import uuid4
|
||||
from kodi_six.utils import py2_decode
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
@@ -23,14 +24,15 @@ class ClientInformation:
|
||||
if client_id:
|
||||
return client_id
|
||||
|
||||
jellyfin_guid_path = xbmc.translatePath("special://temp/jellycon_guid").decode('utf-8')
|
||||
jellyfin_guid_path = py2_decode(xbmc.translatePath("special://temp/jellycon_guid"))
|
||||
log.debug("jellyfin_guid_path: {0}".format(jellyfin_guid_path))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path)
|
||||
client_id = guid.read()
|
||||
guid.close()
|
||||
|
||||
if not client_id:
|
||||
client_id = str("%012X" % uuid4())
|
||||
# Needs to be captilized for backwards compat
|
||||
client_id = uuid4().hex.upper()
|
||||
log.debug("Generating a new guid: {0}".format(client_id))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
|
||||
guid.write(client_id)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
import threading
|
||||
import xbmc
|
||||
|
||||
@@ -16,7 +17,7 @@ class ContextMonitor(threading.Thread):
|
||||
item_id = None
|
||||
log.debug("ContextMonitor Thread Started")
|
||||
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
|
||||
xbmc.sleep(1000)
|
||||
|
||||
@@ -5,8 +5,8 @@ from collections import defaultdict
|
||||
import threading
|
||||
import hashlib
|
||||
import os
|
||||
import cPickle
|
||||
import time
|
||||
from six.moves import cPickle
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .loghandler import LazyLogger
|
||||
@@ -62,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")
|
||||
|
||||
@@ -274,7 +274,8 @@ def clear_cached_server_data():
|
||||
xbmcvfs.delete(os.path.join(addon_dir, filename))
|
||||
del_count += 1
|
||||
|
||||
msg = string_load(30394) % del_count
|
||||
log.debug('Deleted {} files'.format(del_count))
|
||||
msg = string_load(30394)
|
||||
xbmcgui.Dialog().ok(string_load(30393), msg)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote, unquote
|
||||
import sys
|
||||
import re
|
||||
|
||||
@@ -126,7 +126,7 @@ def get_content(url, params):
|
||||
if url_prev:
|
||||
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - page_limit + 1) + "-" + str(start_index) +
|
||||
" of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
u = sys.argv[0] + "?url=" + quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.insert(0, (u, list_item, True))
|
||||
|
||||
@@ -136,7 +136,7 @@ def get_content(url, params):
|
||||
upper_count = total_records
|
||||
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + page_limit + 1) + "-" +
|
||||
str(upper_count) + " of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
u = sys.argv[0] + "?url=" + quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.append((u, list_item, True))
|
||||
|
||||
@@ -173,11 +173,6 @@ def get_content(url, params):
|
||||
else:
|
||||
log.debug("No view id for view type:{0}".format(view_key))
|
||||
|
||||
# send display items event
|
||||
# display_items_notification = {"view_type": view_type}
|
||||
# log.debug("Sending display_items with data {0}", display_items_notification)
|
||||
# send_event_notification("display_items", display_items_notification)
|
||||
|
||||
if progress is not None:
|
||||
progress.update(100, string_load(30125))
|
||||
progress.close()
|
||||
@@ -238,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]
|
||||
|
||||
@@ -7,10 +7,9 @@ import xbmcaddon
|
||||
import requests
|
||||
import hashlib
|
||||
import ssl
|
||||
import StringIO
|
||||
import gzip
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from base64 import b64encode
|
||||
from collections import defaultdict
|
||||
from traceback import format_exc
|
||||
@@ -158,7 +157,6 @@ class DownloadUtils:
|
||||
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
|
||||
# ["hevc", "h265", "h264", "mpeg4", "msmpeg4v3", "mpeg2video", "vc1"]
|
||||
filtered_codecs = []
|
||||
if addon_settings.getSetting("force_transcode_h265") == "true":
|
||||
filtered_codecs.append("hevc")
|
||||
@@ -359,7 +357,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:
|
||||
@@ -387,7 +384,6 @@ 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":
|
||||
@@ -404,14 +400,12 @@ class DownloadUtils:
|
||||
bg_tags = data.get("BackdropImageTags", [])
|
||||
if bg_tags:
|
||||
image_tag = bg_tags[index]
|
||||
# log.debug("Background Image Tag: {0}", imageTag)
|
||||
elif parent is False:
|
||||
image_tags = data.get("ImageTags", [])
|
||||
if image_tags:
|
||||
image_tag_type = image_tags.get(art_type)
|
||||
if image_tag_type:
|
||||
image_tag = image_tag_type
|
||||
# log.debug("Image Tag: {0}", imageTag)
|
||||
elif parent is True:
|
||||
if (item_type == "Episode" or item_type == "Season") and art_type == 'Primary':
|
||||
tag_name = 'SeriesPrimaryImageTag'
|
||||
@@ -424,11 +418,9 @@ class DownloadUtils:
|
||||
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)
|
||||
@@ -436,19 +428,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
|
||||
@@ -578,7 +560,6 @@ class DownloadUtils:
|
||||
log.debug("User Id: {0}".format(userid))
|
||||
window.set_property("AccessToken", access_token)
|
||||
window.set_property("userid", userid)
|
||||
# WINDOW.setProperty("userimage", "")
|
||||
|
||||
self.post_capabilities()
|
||||
|
||||
@@ -599,7 +580,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:
|
||||
@@ -726,7 +707,7 @@ class DownloadUtils:
|
||||
log.error("HTTP response error for {0}: {1} {2}".format(url, data.status_code, data.content))
|
||||
if suppress is False:
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
string_load(30200) % str(data.content),
|
||||
'{}: {}'.format(string_load(30200), data.content),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
try:
|
||||
result = data.json()
|
||||
|
||||
@@ -1,14 +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
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import cProfile
|
||||
import pstats
|
||||
import json
|
||||
import StringIO
|
||||
from six import StringIO
|
||||
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
@@ -34,6 +34,7 @@ from .cache_images import CacheArtwork
|
||||
from .dir_functions import get_content, process_directory
|
||||
from .tracking import timer
|
||||
from .skin_cloner import clone_default_skin
|
||||
from .play_utils import play_file
|
||||
|
||||
__addon__ = xbmcaddon.Addon()
|
||||
__addondir__ = xbmc.translatePath(__addon__.getAddonInfo('profile'))
|
||||
@@ -74,7 +75,7 @@ def main_entry_point():
|
||||
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)
|
||||
|
||||
@@ -302,7 +303,7 @@ def delete(item_id):
|
||||
xbmcgui.Dialog().ok(string_load(30135), string_load(30417), final_name)
|
||||
return
|
||||
|
||||
return_value = xbmcgui.Dialog().yesno(string_load(30091), final_name, string_load(30092))
|
||||
return_value = xbmcgui.Dialog().yesno(string_load(30091), '{}\n{}'.format(final_name, string_load(30092)))
|
||||
if return_value:
|
||||
log.debug('Deleting Item: {0}'.format(item_id))
|
||||
url = '{server}/Items/' + item_id
|
||||
@@ -478,8 +479,6 @@ def show_menu(params):
|
||||
li.setProperty('menu_id', 'set_view')
|
||||
action_items.append(li)
|
||||
|
||||
# xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False)
|
||||
|
||||
action_menu = ActionMenu("ActionMenu.xml", PLUGINPATH, "default", "720p")
|
||||
action_menu.setActionItems(action_items)
|
||||
action_menu.doModal()
|
||||
@@ -492,9 +491,6 @@ def show_menu(params):
|
||||
|
||||
if selected_action == "play":
|
||||
log.debug("Play Item")
|
||||
# list_item = populate_listitem(params["item_id"])
|
||||
# result = xbmcgui.Dialog().info(list_item)
|
||||
# log.debug("xbmcgui.Dialog().info: {0}", result)
|
||||
play_action(params)
|
||||
|
||||
elif selected_action == "set_view":
|
||||
@@ -632,7 +628,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)
|
||||
|
||||
@@ -648,7 +644,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)
|
||||
|
||||
@@ -665,12 +661,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)
|
||||
@@ -751,7 +746,6 @@ def search_results_person(params):
|
||||
person_id = params.get("person_id")
|
||||
details_url = ('{server}/Users/{userid}/items' +
|
||||
'?PersonIds=' + person_id +
|
||||
# '&IncludeItemTypes=Movie' +
|
||||
'&Recursive=true' +
|
||||
'&Fields={field_filters}' +
|
||||
'&format=json')
|
||||
@@ -780,8 +774,6 @@ def search_results_person(params):
|
||||
if content_type:
|
||||
xbmcplugin.setContent(handle, content_type)
|
||||
|
||||
# xbmcplugin.setContent(handle, detected_type)
|
||||
|
||||
if dir_items is not None:
|
||||
xbmcplugin.addDirectoryItems(handle, dir_items)
|
||||
|
||||
@@ -794,7 +786,7 @@ def search_results(params):
|
||||
query_string = params.get('query')
|
||||
if query_string:
|
||||
log.debug("query_string : {0}".format(query_string))
|
||||
query_string = urllib.unquote(query_string)
|
||||
query_string = unquote(query_string)
|
||||
log.debug("query_string : {0}".format(query_string))
|
||||
|
||||
item_type = item_type.lower()
|
||||
@@ -841,7 +833,7 @@ def search_results(params):
|
||||
else:
|
||||
query = query_string
|
||||
|
||||
query = urllib.quote(query)
|
||||
query = quote(query)
|
||||
log.debug("query : {0}".format(query))
|
||||
|
||||
if (not item_type) or (not query):
|
||||
@@ -883,8 +875,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
|
||||
@@ -977,7 +967,7 @@ def play_action(params):
|
||||
play_info["subtitle_stream_index"] = subtitle_stream_index
|
||||
play_info["audio_stream_index"] = audio_stream_index
|
||||
log.info("Sending jellycon_play_action : {0}".format(play_info))
|
||||
send_event_notification("jellycon_play_action", play_info)
|
||||
play_file(play_info)
|
||||
|
||||
|
||||
def play_item_trailer(item_id):
|
||||
@@ -1054,8 +1044,4 @@ def play_item_trailer(item_id):
|
||||
youtube_plugin = "RunPlugin(plugin://plugin.video.youtube/play/?video_id=%s)" % youtube_id
|
||||
log.debug("youtube_plugin: {0}".format(youtube_plugin))
|
||||
|
||||
# play_info = {}
|
||||
# play_info["url"] = youtube_plugin
|
||||
# log.info("Sending jellycon_play_trailer_action : {0}", play_info)
|
||||
# send_event_notification("jellycon_play_youtube_trailer_action", play_info)
|
||||
xbmc.executebuiltin(youtube_plugin)
|
||||
|
||||
@@ -4,13 +4,14 @@ import xbmcvfs
|
||||
import xbmc
|
||||
import base64
|
||||
import re
|
||||
from urlparse import urlparse
|
||||
from random import shuffle
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from six import ensure_text
|
||||
|
||||
import threading
|
||||
import requests
|
||||
import io
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .datamanager import DataManager
|
||||
@@ -35,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")
|
||||
@@ -86,7 +77,7 @@ def build_image(path):
|
||||
if request_path == "favicon.ico":
|
||||
return []
|
||||
|
||||
decoded_url = base64.b64decode(request_path)
|
||||
decoded_url = ensure_text(base64.b64decode(request_path))
|
||||
log.debug("decoded_url : {0}".format(decoded_url))
|
||||
|
||||
image_urls = get_image_links(decoded_url)
|
||||
@@ -109,8 +100,6 @@ def build_image(path):
|
||||
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
# user_name = url_bits.username
|
||||
# user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_query = url_bits.query
|
||||
|
||||
@@ -198,20 +187,22 @@ class HttpImageHandler(BaseHTTPRequestHandler):
|
||||
|
||||
class HttpImageServerThread(threading.Thread):
|
||||
|
||||
keep_running = True
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
self.keep_running = True
|
||||
|
||||
def stop(self):
|
||||
log.debug("HttpImageServerThread:stop called")
|
||||
self.keep_running = False
|
||||
self.server.shutdown()
|
||||
|
||||
def run(self):
|
||||
log.debug("HttpImageServerThread:started")
|
||||
server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
self.server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
|
||||
while self.keep_running:
|
||||
server.handle_request()
|
||||
self.server.serve_forever()
|
||||
xbmc.sleep(1000)
|
||||
|
||||
log.debug("HttpImageServerThread:exiting")
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
|
||||
import sys
|
||||
import os
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -256,7 +256,6 @@ def extract_item_info(item, gui_options):
|
||||
elif person_type == "Writing":
|
||||
item_details.writer = person["Name"]
|
||||
elif person_type == "Actor":
|
||||
# log.debug("Person: {0}", person)
|
||||
person_name = person.get("Name")
|
||||
person_role = person.get("Role")
|
||||
person_id = person.get("Id")
|
||||
@@ -283,7 +282,6 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
# production location
|
||||
prod_location = item.get("ProductionLocations", [])
|
||||
# log.debug("ProductionLocations : {0}", prod_location)
|
||||
if prod_location:
|
||||
item_details.production_location = prod_location[0]
|
||||
|
||||
@@ -318,7 +316,7 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
runtime = item.get("RunTimeTicks")
|
||||
if item_details.is_folder is False and runtime:
|
||||
item_details.duration = long(runtime) / 10000000
|
||||
item_details.duration = runtime / 10000000
|
||||
|
||||
child_count = item.get("ChildCount")
|
||||
if child_count:
|
||||
@@ -358,8 +356,6 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
def add_gui_item(url, item_details, display_options, folder=True, default_sort=False):
|
||||
|
||||
# log.debug("item_details: {0}", item_details.__dict__)
|
||||
|
||||
if not item_details.name:
|
||||
return None
|
||||
|
||||
@@ -370,9 +366,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:
|
||||
@@ -449,8 +445,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
|
||||
@@ -499,8 +493,8 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
if item_details.genres:
|
||||
genres_list = []
|
||||
for genre in item_details.genres:
|
||||
genres_list.append(urllib.quote(genre.encode('utf8')))
|
||||
item_properties["genres"] = urllib.quote("|".join(genres_list))
|
||||
genres_list.append(quote(genre.encode('utf8')))
|
||||
item_properties["genres"] = quote("|".join(genres_list))
|
||||
|
||||
info_labels["genre"] = " / ".join(item_details.genres)
|
||||
|
||||
@@ -568,7 +562,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["trailer"] = "plugin://plugin.video.jellycon?mode=playTrailer&id=" + item_details.id
|
||||
|
||||
list_item.setInfo('video', info_labels)
|
||||
# log.debug("info_labels: {0}", info_labels)
|
||||
|
||||
if item_details.media_streams is not None:
|
||||
for stream in item_details.media_streams:
|
||||
@@ -595,7 +588,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
item_properties["NumEpisodes"] = str(item_details.number_episodes)
|
||||
|
||||
list_item.setRating("imdb", item_details.community_rating, 0, True)
|
||||
# list_item.setRating("rt", item_details.critic_rating, 0, False)
|
||||
item_properties["TotalTime"] = str(item_details.duration)
|
||||
|
||||
else:
|
||||
@@ -606,7 +598,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["artist"] = item_details.song_artist
|
||||
info_labels["album"] = item_details.album_name
|
||||
|
||||
# log.debug("info_labels: {0}", info_labels)
|
||||
list_item.setInfo('music', info_labels)
|
||||
|
||||
list_item.setContentLookup(False)
|
||||
@@ -616,7 +607,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:
|
||||
|
||||
@@ -26,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)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import traceback
|
||||
|
||||
from six import ensure_text
|
||||
from kodi_six import xbmc, xbmcaddon
|
||||
from urlparse import urlparse
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
##################################################################################################
|
||||
|
||||
@@ -48,7 +48,13 @@ class LogHandler(logging.StreamHandler):
|
||||
# Hide server URL in logs
|
||||
string = string.replace(self.server or "{server}", "{jellyfin-server}")
|
||||
|
||||
xbmc.log(string, level=xbmc.LOGNOTICE)
|
||||
py_version = sys.version_info.major
|
||||
# Log level notation changed in Kodi v19
|
||||
if py_version > 2:
|
||||
log_level = xbmc.LOGINFO
|
||||
else:
|
||||
log_level = xbmc.LOGNOTICE
|
||||
xbmc.log(string, level=log_level)
|
||||
|
||||
def _get_log_level(self, level):
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
import urllib
|
||||
from six import ensure_binary, ensure_text
|
||||
from six.moves.urllib.parse import quote
|
||||
import base64
|
||||
import string
|
||||
|
||||
@@ -74,9 +75,9 @@ def show_movie_tags(menu_params):
|
||||
|
||||
item_url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
|
||||
|
||||
content_url = urllib.quote(item_url)
|
||||
content_url = quote(item_url)
|
||||
url = sys.argv[0] + ("?url=" +
|
||||
content_url +
|
||||
"&mode=GET_CONTENT" +
|
||||
@@ -160,9 +161,9 @@ def show_movie_years(menu_params):
|
||||
|
||||
item_url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
|
||||
|
||||
content_url = urllib.quote(item_url)
|
||||
content_url = quote(item_url)
|
||||
url = sys.argv[0] + ("?url=" +
|
||||
content_url +
|
||||
"&mode=GET_CONTENT" +
|
||||
@@ -241,13 +242,13 @@ def show_movie_pages(menu_params):
|
||||
item_data['path'] = item_url
|
||||
item_data['media_type'] = 'movies'
|
||||
|
||||
item_data["art"] = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
|
||||
item_data['art'] = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
|
||||
|
||||
collections.append(item_data)
|
||||
start_index = start_index + page_limit
|
||||
|
||||
for collection in collections:
|
||||
content_url = urllib.quote(collection['path'])
|
||||
content_url = quote(collection['path'])
|
||||
url = sys.argv[0] + ("?url=" + content_url +
|
||||
"&mode=GET_CONTENT" +
|
||||
"&media_type=" + collection["media_type"])
|
||||
@@ -322,14 +323,14 @@ def show_genre_list(menu_params):
|
||||
|
||||
url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(url))))}
|
||||
item_data['art'] = art
|
||||
|
||||
item_data['path'] = url
|
||||
collections.append(item_data)
|
||||
|
||||
for collection in collections:
|
||||
url = sys.argv[0] + ("?url=" + urllib.quote(collection['path']) +
|
||||
url = sys.argv[0] + ("?url=" + quote(collection['path']) +
|
||||
"&mode=GET_CONTENT" +
|
||||
"&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} - {1} - {2}".format(collection.get('title'), url, collection.get("art")))
|
||||
@@ -390,13 +391,13 @@ def show_movie_alpha_list(menu_params):
|
||||
url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
|
||||
item_data['path'] = url
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(url)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(url))))}
|
||||
item_data['art'] = art
|
||||
|
||||
collections.append(item_data)
|
||||
|
||||
for collection in collections:
|
||||
url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) +
|
||||
url = (sys.argv[0] + "?url=" + quote(collection['path']) +
|
||||
"&mode=GET_CONTENT&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} ({1})".format(collection.get('title'), url))
|
||||
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
|
||||
@@ -451,13 +452,13 @@ def show_tvshow_alpha_list(menu_params):
|
||||
|
||||
item_data['path'] = path
|
||||
|
||||
art = {"thumb": "http://localhost:24276/" + base64.b64encode(path)}
|
||||
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(path))))}
|
||||
item_data['art'] = art
|
||||
|
||||
collections.append(item_data)
|
||||
|
||||
for collection in collections:
|
||||
url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) +
|
||||
url = (sys.argv[0] + "?url=" + quote(collection['path']) +
|
||||
"&mode=GET_CONTENT&media_type=" + collection["media_type"])
|
||||
log.debug("addMenuDirectoryItem: {0} ({1})".format(collection.get('title'), url))
|
||||
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
|
||||
@@ -529,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
|
||||
@@ -539,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
|
||||
@@ -553,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)
|
||||
@@ -595,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
|
||||
@@ -603,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
|
||||
@@ -611,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
|
||||
@@ -623,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
|
||||
@@ -635,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
|
||||
@@ -647,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
|
||||
@@ -661,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
|
||||
@@ -694,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
|
||||
@@ -704,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
|
||||
@@ -718,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
|
||||
@@ -732,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
|
||||
@@ -741,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)
|
||||
@@ -761,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)
|
||||
@@ -780,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
|
||||
@@ -791,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
|
||||
@@ -802,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)
|
||||
@@ -834,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 = {}
|
||||
@@ -844,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 = {}
|
||||
@@ -854,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 = {}
|
||||
@@ -867,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 = {}
|
||||
@@ -878,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 = {}
|
||||
@@ -890,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 = {}
|
||||
@@ -902,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)
|
||||
|
||||
@@ -996,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
|
||||
|
||||
|
||||
@@ -1012,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
|
||||
|
||||
|
||||
@@ -1024,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
|
||||
|
||||
|
||||
|
||||
@@ -23,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,6 +9,7 @@ from datetime import timedelta
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .downloadutils import DownloadUtils
|
||||
@@ -18,7 +20,6 @@ from .translation import string_load
|
||||
from .datamanager import DataManager, clear_old_cache_data
|
||||
from .item_functions import extract_item_info, add_gui_item
|
||||
from .clientinfo import ClientInformation
|
||||
from .functions import delete
|
||||
from .cache_images import CacheArtwork
|
||||
from .picture_viewer import PictureViewer
|
||||
from .tracking import timer
|
||||
@@ -28,7 +29,8 @@ 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()
|
||||
|
||||
@@ -88,8 +90,7 @@ def play_all_files(items, monitor, play_items=True):
|
||||
data["playback_type"] = playback_type_string
|
||||
data["play_session_id"] = play_session_id
|
||||
data["play_action_type"] = "play_all"
|
||||
monitor.played_information[playurl] = data
|
||||
log.debug("Add to played_information: {0}".format(monitor.played_information))
|
||||
home_window.set_property('now_playing', json.dumps(data))
|
||||
|
||||
list_item.setPath(playurl)
|
||||
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
|
||||
@@ -103,7 +104,7 @@ def play_all_files(items, monitor, play_items=True):
|
||||
return playlist
|
||||
|
||||
|
||||
def play_list_of_items(id_list, monitor):
|
||||
def play_list_of_items(id_list):
|
||||
log.debug("Loading all items in the list")
|
||||
data_manager = DataManager()
|
||||
items = []
|
||||
@@ -117,10 +118,10 @@ def play_list_of_items(id_list, monitor):
|
||||
return
|
||||
items.append(result)
|
||||
|
||||
return play_all_files(items, monitor)
|
||||
return play_all_files(items)
|
||||
|
||||
|
||||
def add_to_playlist(play_info, monitor):
|
||||
def add_to_playlist(play_info):
|
||||
log.debug("Adding item to playlist : {0}".format(play_info))
|
||||
|
||||
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
@@ -152,7 +153,6 @@ def add_to_playlist(play_info, monitor):
|
||||
play_session_id = playback_info.get("PlaySessionId")
|
||||
|
||||
# select the media source to use
|
||||
# sources = item.get("MediaSources")
|
||||
sources = playback_info.get('MediaSources')
|
||||
|
||||
selected_media_source = sources[0]
|
||||
@@ -187,8 +187,6 @@ def add_to_playlist(play_info, monitor):
|
||||
data["playback_type"] = playback_type_string
|
||||
data["play_session_id"] = play_session_id
|
||||
data["play_action_type"] = "play_all"
|
||||
monitor.played_information[playurl] = data
|
||||
log.debug("Add to played_information: {0}".format(monitor.played_information))
|
||||
|
||||
list_item.setPath(playurl)
|
||||
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
|
||||
@@ -215,7 +213,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 +223,12 @@ def play_file(play_info, monitor):
|
||||
|
||||
action = play_info.get("action", "play")
|
||||
if action == "add_to_playlist":
|
||||
add_to_playlist(play_info, monitor)
|
||||
add_to_playlist(play_info)
|
||||
return
|
||||
|
||||
# if this is a list of items them add them all to the play list
|
||||
if isinstance(item_id, list):
|
||||
return play_list_of_items(item_id, monitor)
|
||||
return play_list_of_items(item_id)
|
||||
|
||||
auto_resume = play_info.get("auto_resume", "-1")
|
||||
force_transcode = play_info.get("force_transcode", False)
|
||||
@@ -272,7 +270,7 @@ def play_file(play_info, monitor):
|
||||
items = result["Items"]
|
||||
if items is None:
|
||||
items = []
|
||||
return play_all_files(items, monitor)
|
||||
return play_all_files(items)
|
||||
|
||||
# if this is a program from live tv epg then play the actual channel
|
||||
if result.get("Type") == "Program":
|
||||
@@ -368,22 +366,6 @@ def play_file(play_info, monitor):
|
||||
del resume_dialog
|
||||
log.debug("Resume Dialog Result: {0}".format(resume_result))
|
||||
|
||||
# check system settings for play action
|
||||
# if prompt is set ask to set it to auto resume
|
||||
# remove for now as the context dialog is now handeled in the monitor thread
|
||||
# params = {"setting": "myvideos.selectaction"}
|
||||
# setting_result = json_rpc('Settings.getSettingValue').execute(params)
|
||||
# log.debug("Current Setting (myvideos.selectaction): {0}", setting_result)
|
||||
# current_value = setting_result.get("result", None)
|
||||
# if current_value is not None:
|
||||
# current_value = current_value.get("value", -1)
|
||||
# if current_value not in (2,3):
|
||||
# return_value = xbmcgui.Dialog().yesno(string_load(30276), string_load(30277))
|
||||
# if return_value:
|
||||
# params = {"setting": "myvideos.selectaction", "value": 2}
|
||||
# json_rpc_result = json_rpc('Settings.setSettingValue').execute(params)
|
||||
# log.debug("Save Setting (myvideos.selectaction): {0}", json_rpc_result)
|
||||
|
||||
if resume_result == 1:
|
||||
seek_time = 0
|
||||
elif resume_result == -1:
|
||||
@@ -445,8 +427,7 @@ def play_file(play_info, monitor):
|
||||
data["play_action_type"] = "play"
|
||||
data["item_type"] = result.get("Type", None)
|
||||
data["can_delete"] = result.get("CanDelete", False)
|
||||
monitor.played_information[playurl] = data
|
||||
log.debug("Add to played_information: {0}".format(monitor.played_information))
|
||||
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 +439,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)
|
||||
@@ -524,7 +505,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', ''),
|
||||
@@ -657,9 +637,6 @@ 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')
|
||||
@@ -891,8 +868,10 @@ def external_subs(media_source, list_item, item_id):
|
||||
list_item.setSubtitles([selected_sub])
|
||||
|
||||
|
||||
def send_progress(monitor):
|
||||
play_data = get_playing_data(monitor.played_information)
|
||||
def send_progress():
|
||||
home_window = HomeWindow()
|
||||
play_data_string = home_window.get_property('now_playing')
|
||||
play_data = json.loads(play_data_string)
|
||||
|
||||
if play_data is None:
|
||||
return
|
||||
@@ -987,7 +966,6 @@ def prompt_for_stop_actions(item_id, data):
|
||||
return
|
||||
|
||||
# item percentage complete
|
||||
# percenatge_complete = int(((current_position * 10000000) / runtime) * 100)
|
||||
percenatge_complete = int((current_position / duration) * 100)
|
||||
log.debug("Episode Percentage Complete: {0}".format(percenatge_complete))
|
||||
|
||||
@@ -1003,21 +981,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))
|
||||
@@ -1029,25 +1000,6 @@ 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}".format(played_information))
|
||||
@@ -1095,21 +1047,18 @@ def stop_all_playback(played_information):
|
||||
download_utils.download_url(url, method="DELETE")
|
||||
|
||||
|
||||
def get_playing_data(play_data_map):
|
||||
def get_playing_data():
|
||||
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}".format(e))
|
||||
return None
|
||||
log.debug("get_playing_data : getPlayingFile() : {0}".format(playing_file))
|
||||
if playing_file not in play_data_map:
|
||||
infolabel_path_and_file = xbmc.getInfoLabel("Player.Filenameandpath")
|
||||
log.debug("get_playing_data : Filenameandpath : {0}".format(infolabel_path_and_file))
|
||||
if infolabel_path_and_file not in play_data_map:
|
||||
log.debug("get_playing_data : play data not found")
|
||||
return None
|
||||
else:
|
||||
playing_file = infolabel_path_and_file
|
||||
if server in playing_file:
|
||||
url_data = urlparse(playing_file)
|
||||
query = parse_qs(url_data.query)
|
||||
|
||||
return play_data_map.get(playing_file)
|
||||
|
||||
@@ -1128,7 +1077,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
|
||||
@@ -1177,26 +1126,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):
|
||||
@@ -1209,10 +1158,13 @@ class PlaybackService(xbmc.Monitor):
|
||||
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':
|
||||
return
|
||||
@@ -1252,13 +1204,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'
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
|
||||
import socket
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
from six.moves.urllib.parse import urlparse
|
||||
import requests
|
||||
import ssl
|
||||
import time
|
||||
@@ -125,23 +125,20 @@ def get_server_details():
|
||||
log.debug("Getting Server Details from Network")
|
||||
servers = []
|
||||
|
||||
message = "who is JellyfinServer?"
|
||||
message = b"who is JellyfinServer?"
|
||||
multi_group = ("<broadcast>", 7359)
|
||||
# multi_group = ("127.0.0.1", 7359)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(4.0)
|
||||
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 3) # timeout
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
|
||||
|
||||
log.debug("MutliGroup: {0}".format(multi_group))
|
||||
log.debug("Sending UDP Data: {0}".format(message))
|
||||
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(__addon_name__ + " : " + string_load(30373))
|
||||
progress.create('{} : {}'.format(__addon_name__, string_load(30373)))
|
||||
progress.update(0, string_load(30374))
|
||||
xbmc.sleep(1000)
|
||||
server_count = 0
|
||||
@@ -152,7 +149,7 @@ def get_server_details():
|
||||
while True:
|
||||
try:
|
||||
server_count += 1
|
||||
progress.update(server_count * 10, string_load(30375) % server_count)
|
||||
progress.update(server_count * 10, '{}: {}'.format(string_load(30375), server_count))
|
||||
xbmc.sleep(1000)
|
||||
data, addr = sock.recvfrom(1024)
|
||||
servers.append(json.loads(data))
|
||||
@@ -201,14 +198,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
|
||||
@@ -231,17 +228,17 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
|
||||
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))
|
||||
result = du.download_url(public_lookup_url, authenticate=False)
|
||||
progress.close()
|
||||
|
||||
if result:
|
||||
xbmcgui.Dialog().ok(__addon_name__ + " : " + string_load(30167),
|
||||
xbmcgui.Dialog().ok('{} : {}'.format(__addon_name__, string_load(30167)),
|
||||
server_url)
|
||||
break
|
||||
else:
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__ + " : " + string_load(30135),
|
||||
return_index = xbmcgui.Dialog().yesno('{} : {}'.format(__addon_name__, string_load(30135)),
|
||||
server_url,
|
||||
string_load(30371))
|
||||
if not return_index:
|
||||
@@ -255,7 +252,7 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
# do we need to change the user
|
||||
user_details = load_user_details(settings)
|
||||
current_username = user_details.get("username", "")
|
||||
current_username = unicode(current_username, "utf-8")
|
||||
current_username = py2_decode(current_username)
|
||||
|
||||
# if asked or we have no current user then show user selection screen
|
||||
if something_changed or change_user or len(current_username) == 0:
|
||||
@@ -384,7 +381,7 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
if secured:
|
||||
# we need a password, check the settings first
|
||||
m = hashlib.md5()
|
||||
m.update(selected_user_name)
|
||||
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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote, unquote
|
||||
import encodings
|
||||
|
||||
import xbmc
|
||||
@@ -20,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):
|
||||
@@ -106,7 +106,7 @@ def get_episode_id(parent_id, episode):
|
||||
|
||||
|
||||
def get_match(item_type, title, year, imdb_id):
|
||||
query = urllib.quote(title)
|
||||
query = quote(title)
|
||||
|
||||
results = search(item_type, query=query)
|
||||
results = results.get('SearchHints')
|
||||
@@ -138,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', '')
|
||||
@@ -246,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))
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
|
||||
import xbmcaddon
|
||||
from .loghandler import LazyLogger
|
||||
from kodi_six.utils import py2_encode
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
addon = xbmcaddon.Addon()
|
||||
@@ -9,7 +10,7 @@ addon = xbmcaddon.Addon()
|
||||
|
||||
def string_load(string_id):
|
||||
try:
|
||||
return addon.getLocalizedString(string_id).encode('utf-8', 'ignore')
|
||||
return py2_encode(addon.getLocalizedString(string_id))
|
||||
except Exception as e:
|
||||
log.error('Failed String Load: {0} ({1})', string_id, e)
|
||||
return str(string_id)
|
||||
|
||||
@@ -7,7 +7,6 @@ import xbmcvfs
|
||||
|
||||
import string
|
||||
import random
|
||||
import urllib
|
||||
import json
|
||||
import base64
|
||||
import time
|
||||
@@ -15,7 +14,7 @@ import math
|
||||
from datetime import datetime
|
||||
import calendar
|
||||
import re
|
||||
from urllib import urlencode
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .loghandler import LazyLogger
|
||||
@@ -134,7 +133,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)
|
||||
|
||||
@@ -217,7 +216,6 @@ def get_art(item, server):
|
||||
|
||||
image_tags = item.get("ImageTags", {})
|
||||
if image_tags and image_tags.get("Primary"):
|
||||
# image_tag = image_tags["Primary"]
|
||||
art['thumb'] = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
|
||||
item_type = item["Type"]
|
||||
@@ -226,7 +224,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)
|
||||
@@ -293,7 +290,7 @@ def double_urlencode(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
|
||||
|
||||
@@ -301,7 +298,7 @@ def single_urlencode(text):
|
||||
def send_event_notification(method, data):
|
||||
message_data = json.dumps(data)
|
||||
source_id = "jellycon"
|
||||
base64_data = base64.b64encode(message_data)
|
||||
base64_data = base64.b64encode(message_data.encode())
|
||||
escaped_data = '\\"[\\"{0}\\"]\\"'.format(base64_data)
|
||||
command = 'XBMC.NotifyAll({0}.SIGNAL,{1},{2})'.format(source_id, method, escaped_data)
|
||||
log.debug("Sending notification event data: {0}".format(command))
|
||||
|
||||
@@ -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,6 +2,7 @@
|
||||
|
||||
#################################################################################################
|
||||
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
import json
|
||||
import threading
|
||||
import websocket
|
||||
@@ -257,11 +258,13 @@ class WebSocketClient(threading.Thread):
|
||||
websocket_url = "%s/socket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
|
||||
log.debug("websocket url: {0}".format(websocket_url))
|
||||
|
||||
self._client = websocket.WebSocketApp(websocket_url,
|
||||
on_open=self.on_open,
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_close=self.on_close)
|
||||
self._client = websocket.WebSocketApp(
|
||||
websocket_url,
|
||||
on_open=lambda ws, message: self.on_open(ws),
|
||||
on_message=lambda ws, message: self.on_message(ws, message),
|
||||
on_error=lambda ws, error: self.on_error(ws, error),
|
||||
on_close=lambda ws, error: self.on_close(ws))
|
||||
|
||||
log.debug("Starting WebSocketClient")
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
|
||||
@@ -55,7 +55,7 @@ def set_random_movies():
|
||||
movies_list_string = ",".join(randon_movies_list)
|
||||
home_window = HomeWindow()
|
||||
m = hashlib.md5()
|
||||
m.update(movies_list_string)
|
||||
m.update(movies_list_string.encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
|
||||
log.debug("set_random_movies : {0}".format(movies_list_string))
|
||||
@@ -176,7 +176,6 @@ def check_for_new_content():
|
||||
items = result.get("Items", [])
|
||||
if len(items) > 0:
|
||||
item = items[0]
|
||||
# last_played_date = item.get("Etag", "")
|
||||
user_data = item.get("UserData", None)
|
||||
if user_data is not None:
|
||||
last_played_date = user_data.get("LastPlayedDate", "")
|
||||
@@ -222,12 +221,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")
|
||||
@@ -372,17 +365,11 @@ def get_widget_content(handle, params):
|
||||
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):
|
||||
@@ -405,8 +392,6 @@ def get_widget_content(handle, params):
|
||||
filtered_list.append(item)
|
||||
list_items = filtered_list
|
||||
|
||||
# list_items = populateWidgetItems(items_url, widget_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}".format(detected_type))
|
||||
|
||||
15
service.py
15
service.py
@@ -67,7 +67,6 @@ image_server = HttpImageServerThread()
|
||||
image_server.start()
|
||||
|
||||
# set up all the services
|
||||
monitor = Service()
|
||||
playback_service = PlaybackService(monitor)
|
||||
|
||||
home_window = HomeWindow()
|
||||
@@ -112,13 +111,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 +123,7 @@ while not xbmc.abortRequested:
|
||||
# if playing every 10 seconds updated the server with progress
|
||||
if (time.time() - last_progress_update) > 10:
|
||||
last_progress_update = time.time()
|
||||
send_progress(monitor)
|
||||
send_progress()
|
||||
|
||||
else:
|
||||
screen_saver_active = xbmc.getCondVisibility("System.ScreenSaverActive")
|
||||
@@ -189,6 +186,9 @@ while not xbmc.abortRequested:
|
||||
|
||||
image_server.stop()
|
||||
|
||||
# stop the WebSocket Client
|
||||
websocket_client.stop_client()
|
||||
|
||||
# call stop on the library update monitor
|
||||
library_change_monitor.stop()
|
||||
|
||||
@@ -200,9 +200,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")
|
||||
|
||||
Reference in New Issue
Block a user