42 Commits

Author SHA1 Message Date
mcarlton00
9199eb4290 Merge pull request #65 from mcarlton00/version-bump-0.4.3
Version bump
2021-04-18 11:19:32 -04:00
Matt
8831af3fb4 Version bump 2021-04-18 11:15:27 -04:00
mcarlton00
20b1686b04 Merge pull request #63 from mcarlton00/public-users
Fix displaying public user list
2021-04-18 11:05:11 -04:00
mcarlton00
ae028f485a Merge pull request #62 from danieladov/master
Change unicode to str
2021-04-18 11:04:48 -04:00
Matt
d5af0c8d7e Fix display public user list 2021-04-14 19:37:41 -04:00
Mister Rajoy
e596998a72 Change str to ensure_text to keep compatibility 2021-03-18 16:57:39 +01:00
Mister Rajoy
f224c0b94a Change unicode to str
Python 3 renamed the unicode type to str
2021-03-17 17:42:18 +01:00
Abby
bc06467784 Merge pull request #61 from mcarlton00/version-bump-0.4.2
Version bump to 0.4.2
2021-03-16 01:08:27 +00:00
Matt
b2f369de10 Version bump 2021-03-15 20:57:48 -04:00
mcarlton00
0e070308db Merge pull request #60 from danieladov/master
Remove multicast socket options from autodiscovery
2021-03-15 10:42:48 -04:00
Mister Rajoy
1b7c3ffae0 Remove multicast socket options from autodiscovery 2021-03-15 15:36:23 +01:00
mcarlton00
1069bf73e7 Merge pull request #58 from mcarlton00/version-0.4.1
Version bump
2021-03-08 17:10:57 -05:00
Matt
483b708def Version bump 2021-03-08 17:08:21 -05:00
mcarlton00
be12c0d21f Merge pull request #57 from mcarlton00/strings-bytes
Fix browsing by pages
2021-03-06 12:51:29 -05:00
Matt
bc57964aed Fix browsing by pages 2021-03-04 19:02:24 -05:00
mcarlton00
a6f2abaab9 Merge pull request #55 from mcarlton00/version-bump-0.4.0
Version bump - 0.4.0
2021-03-02 21:01:10 -05:00
Matt
304ff1a42c Version bump 2021-03-02 20:57:55 -05:00
mcarlton00
a5048b317d Merge pull request #54 from mcarlton00/matrix-dialogs
Fix yes/no dialogs in kodi 19
2021-02-28 19:58:22 -05:00
Matt
f42b5c2a99 Fix yes/no dialogs in kodi 19 2021-02-28 19:29:55 -05:00
mcarlton00
5827b42732 Merge pull request #53 from mcarlton00/build-script
Add build script and set up pipeline
2021-02-28 18:55:40 -05:00
Matt
6e62571cce Fix folder name in build pipeline 2021-02-28 17:18:06 -05:00
Matt
a68e42657f Remove commented code in build script 2021-02-28 17:11:42 -05:00
Matt
bad47421c0 Add build script and set up pipeline 2021-02-28 16:58:59 -05:00
mcarlton00
757f0a411c Merge pull request #52 from mcarlton00/future-imports
Use future imports for all library files
2021-02-25 20:12:47 -05:00
Matt
cba411658f Use future imports for all library files 2021-02-25 20:00:26 -05:00
mcarlton00
e560b1e591 Merge pull request #50 from mcarlton00/py3
Add support for Kodi Matrix
2021-02-24 21:52:56 -05:00
Matt
e280b82582 Fix sonarcloud bugs 2021-02-16 18:54:46 -05:00
Matt
a49900a2d7 More commented out code 2021-02-14 11:17:42 -05:00
Matt
8ece4ae651 Remove commented blocks of code 2021-02-14 11:14:03 -05:00
Matt
1949e8a9b7 Use upstream websockets library 2021-02-13 23:11:27 -05:00
Matt
52207a5ed8 Update cache dialog box for kodi 19 2021-02-13 19:19:15 -05:00
Matt
f90db72f8b End playback monitoring thread on Kodi exit 2021-01-30 23:28:01 -05:00
Matt
d298b4caa2 Fix hanging Kodi on exit 2021-01-30 23:27:16 -05:00
Matt
8109f5ae41 Move to upstream websocket library 2021-01-30 23:26:12 -05:00
Matt
e4ba7b0eba Fix deprecated abort system 2021-01-26 22:35:37 -05:00
Matt
ed3087a222 String manipulations and encoding fixes 2021-01-26 22:34:51 -05:00
Matt
c6f6601f3c Working playback 2021-01-26 22:34:39 -05:00
Matt
fb6a1c1329 Initial py3 pass 2021-01-02 23:04:24 -05:00
mcarlton00
920c012338 Merge pull request #44 from mcarlton00/translation-variable-replacements
Stop doing string manipulations on translations
2021-01-02 23:02:02 -05:00
Matt
b629756f3e Add log message for deleting items 2021-01-02 18:18:21 -05:00
Matt
0cf4643d5f Remove %s from languages file 2021-01-02 18:13:07 -05:00
Matt
73d757122a Stop doing string manipulations on translations 2021-01-02 18:10:59 -05:00
34 changed files with 520 additions and 1291 deletions

22
.ci/azure-pipelines.yml Normal file
View 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
View 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
View 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
View 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)

View File

@@ -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
View File

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

108
build.py Executable file
View 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)

37
release.yaml Normal file
View File

@@ -0,0 +1,37 @@
version: '0.4.3'
changelog: |
- #62 - Change unicode to str
- #63 - Fix displaying public user list
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
View File

@@ -0,0 +1 @@
pyyaml

View File

@@ -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"

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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]

View File

@@ -7,14 +7,14 @@ 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
from kodi_six.utils import py2_decode
from six import ensure_text
from .kodi_utils import HomeWindow
from .clientinfo import ClientInformation
@@ -158,7 +158,6 @@ class DownloadUtils:
addon_settings = xbmcaddon.Addon()
# ["hevc", "h265", "h264", "mpeg4", "msmpeg4v3", "mpeg2video", "vc1"]
filtered_codecs = []
if addon_settings.getSetting("force_transcode_h265") == "true":
filtered_codecs.append("hevc")
@@ -359,7 +358,6 @@ class DownloadUtils:
item_id = item["Id"]
item_type = item["Type"]
image_tags = item["ImageTags"]
# bg_item_tags = item["ParentBackdropImageTags"]
# All the image tags
for tag_name in image_tags:
@@ -387,7 +385,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 +401,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 +419,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 +429,9 @@ class DownloadUtils:
if self.use_https and not self.verify_cert:
artwork += "|verifypeer=false"
# log.debug("getArtwork: request:{0} item:{1} parent:{2} link:{3}", art_type, item_type, parent, artwork)
'''
# do not return non-existing images
if ( (art_type != "Backdrop" and imageTag == "") |
(art_type == "Backdrop" and data.get("BackdropImageTags") != None and len(data.get("BackdropImageTags")) == 0) |
(art_type == "Backdrop" and data.get("BackdropImageTag") != None and len(data.get("BackdropImageTag")) == 0)
):
artwork = ''
'''
return artwork
def image_url(self, item_id, art_type, index, width, height, image_tag, server):
# test imageTag e3ab56fe27d389446754d0fb04910a34
@@ -511,7 +494,7 @@ class DownloadUtils:
secure = False
for user in result:
if user.get("Name") == unicode(user_name, "utf-8"):
if user.get("Name") == ensure_text(user_name, "utf-8"):
userid = user.get("Id")
user_image = self.get_user_artwork(user, 'Primary')
log.debug("Username Found: {0}".format(user.get("Name")))
@@ -578,7 +561,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 +581,7 @@ class DownloadUtils:
settings = xbmcaddon.Addon()
device_name = settings.getSetting('deviceName')
# remove none ascii chars
device_name = device_name.decode("ascii", errors='ignore')
device_name = py2_decode(device_name)
# remove some chars not valid for names
device_name = device_name.replace("\"", "_")
if len(device_name) == 0:
@@ -726,7 +708,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()

View File

@@ -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)

View File

@@ -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")

View File

@@ -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
@@ -16,6 +16,7 @@ from .utils import get_art, datetime_from_string
from .loghandler import LazyLogger
from .downloadutils import DownloadUtils
from .kodi_utils import HomeWindow
from six import ensure_text
log = LazyLogger(__name__)
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
@@ -178,7 +179,7 @@ def extract_item_info(item, gui_options):
name_info["SeasonIndex"] = u"%02d" % item_details.season_number
name_info["EpisodeIndex"] = u"%02d" % item_details.episode_number
log.debug("FormatName: {0} | {1}".format(name_format, name_info))
item_details.name = unicode(name_format).format(**name_info).strip()
item_details.name = ensure_text(name_format).format(**name_info).strip()
year = item.get("ProductionYear")
prem_date = item.get("PremiereDate")
@@ -256,7 +257,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 +283,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 +317,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 +357,6 @@ def extract_item_info(item, gui_options):
def add_gui_item(url, item_details, display_options, folder=True, default_sort=False):
# log.debug("item_details: {0}", item_details.__dict__)
if not item_details.name:
return None
@@ -370,9 +367,9 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
# Create the URL to pass to the item
if folder:
u = sys.argv[0] + "?url=" + urllib.quote(url) + mode + "&media_type=" + item_details.item_type
u = sys.argv[0] + "?url=" + quote(url) + mode + "&media_type=" + item_details.item_type
if item_details.name_format:
u += '&name_format=' + urllib.quote(item_details.name_format)
u += '&name_format=' + quote(item_details.name_format)
if default_sort:
u += '&sort=none'
else:
@@ -449,8 +446,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
else:
list_item = xbmcgui.ListItem(list_item_name, iconImage=thumb_path, thumbnailImage=thumb_path)
# log.debug("Setting thumbnail as: {0}", thumbPath)
item_properties = {}
# calculate percentage
@@ -499,8 +494,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 +563,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
info_labels["trailer"] = "plugin://plugin.video.jellycon?mode=playTrailer&id=" + item_details.id
list_item.setInfo('video', info_labels)
# log.debug("info_labels: {0}", info_labels)
if item_details.media_streams is not None:
for stream in item_details.media_streams:
@@ -595,7 +589,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
item_properties["NumEpisodes"] = str(item_details.number_episodes)
list_item.setRating("imdb", item_details.community_rating, 0, True)
# list_item.setRating("rt", item_details.critic_rating, 0, False)
item_properties["TotalTime"] = str(item_details.duration)
else:
@@ -606,7 +599,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
info_labels["artist"] = item_details.song_artist
info_labels["album"] = item_details.album_name
# log.debug("info_labels: {0}", info_labels)
list_item.setInfo('music', info_labels)
list_item.setContentLookup(False)
@@ -616,7 +608,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
if item_details.baseline_itemname is not None:
item_properties["suggested_from_watching"] = item_details.baseline_itemname
# log.debug("item_properties: {0}", item_properties)
if kodi_version > 17:
list_item.setProperties(item_properties)
else:

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcgui
@@ -8,6 +9,7 @@ from datetime import timedelta
import json
import os
import base64
from six.moves.urllib.parse import urlparse
from .loghandler import LazyLogger
from .downloadutils import DownloadUtils
@@ -18,7 +20,6 @@ from .translation import string_load
from .datamanager import DataManager, clear_old_cache_data
from .item_functions import extract_item_info, add_gui_item
from .clientinfo import ClientInformation
from .functions import delete
from .cache_images import CacheArtwork
from .picture_viewer import PictureViewer
from .tracking import timer
@@ -28,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'

View File

@@ -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
@@ -13,6 +13,7 @@ from datetime import datetime
import xbmcaddon
import xbmcgui
import xbmc
from six import ensure_binary
from kodi_six.utils import py2_decode
from .kodi_utils import HomeWindow
@@ -125,23 +126,20 @@ def get_server_details():
log.debug("Getting Server Details from Network")
servers = []
message = "who is JellyfinServer?"
message = b"who is JellyfinServer?"
multi_group = ("<broadcast>", 7359)
# multi_group = ("127.0.0.1", 7359)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(4.0)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 3) # timeout
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
log.debug("MutliGroup: {0}".format(multi_group))
log.debug("Sending UDP Data: {0}".format(message))
progress = xbmcgui.DialogProgress()
progress.create(__addon_name__ + " : " + string_load(30373))
progress.create('{} : {}'.format(__addon_name__, string_load(30373)))
progress.update(0, string_load(30374))
xbmc.sleep(1000)
server_count = 0
@@ -152,7 +150,7 @@ def get_server_details():
while True:
try:
server_count += 1
progress.update(server_count * 10, string_load(30375) % server_count)
progress.update(server_count * 10, '{}: {}'.format(string_load(30375), server_count))
xbmc.sleep(1000)
data, addr = sock.recvfrom(1024)
servers.append(json.loads(data))
@@ -201,14 +199,14 @@ def check_server(force=False, change_user=False, notify=False):
server_list.append(server_item)
if len(server_list) > 0:
return_index = xbmcgui.Dialog().select(__addon_name__ + " : " + string_load(30166),
return_index = xbmcgui.Dialog().select('{} : {}'.format(__addon_name__, string_load(30166)),
server_list,
useDetails=True)
if return_index != -1:
server_url = server_info[return_index]["Address"]
if not server_url:
return_index = xbmcgui.Dialog().yesno(__addon_name__, string_load(30282), string_load(30370))
return_index = xbmcgui.Dialog().yesno(__addon_name__, '{}\n{}'.format(string_load(30282), string_load(30370)))
if not return_index:
xbmc.executebuiltin("ActivateWindow(Home)")
return
@@ -231,17 +229,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 +253,7 @@ def check_server(force=False, change_user=False, notify=False):
# do we need to change the user
user_details = load_user_details(settings)
current_username = user_details.get("username", "")
current_username = unicode(current_username, "utf-8")
current_username = py2_decode(current_username)
# if asked or we have no current user then show user selection screen
if something_changed or change_user or len(current_username) == 0:
@@ -318,7 +316,7 @@ def check_server(force=False, change_user=False, notify=False):
user_item.setProperty("secure", "true")
m = hashlib.md5()
m.update(name)
m.update(ensure_binary(name))
hashed_username = m.hexdigest()
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
if saved_password:
@@ -384,7 +382,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"

View File

@@ -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))

View File

@@ -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)

View File

@@ -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))

View File

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

View File

@@ -2,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():

View File

@@ -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))

View File

@@ -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")