mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-22 22:23:36 -07:00
Merge branch 'nightly' into dependabot/pip/nightly/distro-1.8.0
This commit is contained in:
commit
2875e15ce2
25 changed files with 116 additions and 104 deletions
|
@ -226,7 +226,7 @@ DOCUMENTATION :: END
|
||||||
getExportFields();
|
getExportFields();
|
||||||
|
|
||||||
$('#export_file_format').on('change', function() {
|
$('#export_file_format').on('change', function() {
|
||||||
if ($(this).val() === 'm3u8') {
|
if ($(this).val() === 'm3u') {
|
||||||
$('#export_metadata_level').prop('disabled', true);
|
$('#export_metadata_level').prop('disabled', true);
|
||||||
$('#export_media_info_level').prop('disabled', true);
|
$('#export_media_info_level').prop('disabled', true);
|
||||||
$("#export_thumb_level").prop('disabled', true);
|
$("#export_thumb_level").prop('disabled', true);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
from copy import deepcopy
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
import numbers
|
import numbers
|
||||||
import certifi
|
import os
|
||||||
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
|
import certifi
|
||||||
from six import python_2_unicode_compatible, add_metaclass
|
from six import python_2_unicode_compatible, add_metaclass
|
||||||
|
|
||||||
logger = logging.getLogger("Cloudinary")
|
logger = logging.getLogger("Cloudinary")
|
||||||
|
@ -23,7 +23,7 @@ from cloudinary.cache import responsive_breakpoints_cache
|
||||||
from cloudinary.http_client import HttpClient
|
from cloudinary.http_client import HttpClient
|
||||||
from cloudinary.compat import urlparse, parse_qs
|
from cloudinary.compat import urlparse, parse_qs
|
||||||
|
|
||||||
from platform import python_version
|
from platform import python_version, platform
|
||||||
|
|
||||||
CERT_KWARGS = {
|
CERT_KWARGS = {
|
||||||
'cert_reqs': 'CERT_REQUIRED',
|
'cert_reqs': 'CERT_REQUIRED',
|
||||||
|
@ -38,15 +38,17 @@ CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAA
|
||||||
URI_SCHEME = "cloudinary"
|
URI_SCHEME = "cloudinary"
|
||||||
API_VERSION = "v1_1"
|
API_VERSION = "v1_1"
|
||||||
|
|
||||||
VERSION = "1.29.0"
|
VERSION = "1.30.0"
|
||||||
|
|
||||||
USER_AGENT = "CloudinaryPython/{} (Python {})".format(VERSION, python_version())
|
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
||||||
|
|
||||||
|
USER_AGENT = "CloudinaryPython/{} ({})".format(VERSION, _USER_PLATFORM_DETAILS)
|
||||||
""" :const: USER_AGENT """
|
""" :const: USER_AGENT """
|
||||||
|
|
||||||
USER_PLATFORM = ""
|
USER_PLATFORM = ""
|
||||||
"""
|
"""
|
||||||
Additional information to be passed with the USER_AGENT, e.g. "CloudinaryMagento/1.0.1".
|
Additional information to be passed with the USER_AGENT, e.g. "CloudinaryCLI/1.2.3".
|
||||||
This value is set in platform-specific implementations that use cloudinary_php.
|
This value is set in platform-specific implementations that use pycloudinary.
|
||||||
|
|
||||||
The format of the value should be <ProductName>/Version[ (comment)].
|
The format of the value should be <ProductName>/Version[ (comment)].
|
||||||
@see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
|
@see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
|
||||||
|
|
|
@ -93,6 +93,23 @@ def resources_by_ids(public_ids, **options):
|
||||||
return call_api("get", uri, params, **options)
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
def resources_by_asset_folder(asset_folder, **options):
|
||||||
|
"""
|
||||||
|
Returns the details of the resources (assets) under a specified asset_folder.
|
||||||
|
|
||||||
|
:param asset_folder: The Asset Folder of the asset
|
||||||
|
:type asset_folder: string
|
||||||
|
:param options: Additional options
|
||||||
|
:type options: dict, optional
|
||||||
|
:return: Resources (assets) of a specific asset_folder
|
||||||
|
:rtype: Response
|
||||||
|
"""
|
||||||
|
uri = ["resources", "by_asset_folder"]
|
||||||
|
params = only(options, "max_results", "tags", "moderations", "context", "next_cursor")
|
||||||
|
params["asset_folder"] = asset_folder
|
||||||
|
return call_api("get", uri, params, **options)
|
||||||
|
|
||||||
|
|
||||||
def resources_by_asset_ids(asset_ids, **options):
|
def resources_by_asset_ids(asset_ids, **options):
|
||||||
"""Retrieves the resources (assets) indicated in the asset IDs.
|
"""Retrieves the resources (assets) indicated in the asset IDs.
|
||||||
This method does not return deleted assets even if they have been backed up.
|
This method does not return deleted assets even if they have been backed up.
|
||||||
|
@ -131,7 +148,7 @@ def resources_by_context(key, value=None, **options):
|
||||||
resource_type = options.pop("resource_type", "image")
|
resource_type = options.pop("resource_type", "image")
|
||||||
uri = ["resources", resource_type, "context"]
|
uri = ["resources", resource_type, "context"]
|
||||||
params = only(options, "next_cursor", "max_results", "tags",
|
params = only(options, "next_cursor", "max_results", "tags",
|
||||||
"context", "moderations", "direction", "metadata")
|
"context", "moderations", "direction", "metadata")
|
||||||
params["key"] = key
|
params["key"] = key
|
||||||
if value is not None:
|
if value is not None:
|
||||||
params["value"] = value
|
params["value"] = value
|
||||||
|
@ -194,10 +211,18 @@ def update(public_id, **options):
|
||||||
options.get("custom_coordinates"))
|
options.get("custom_coordinates"))
|
||||||
if "context" in options:
|
if "context" in options:
|
||||||
params["context"] = utils.encode_context(options.get("context"))
|
params["context"] = utils.encode_context(options.get("context"))
|
||||||
|
if "metadata" in options:
|
||||||
|
params["metadata"] = utils.encode_context(options.get("metadata"))
|
||||||
if "auto_tagging" in options:
|
if "auto_tagging" in options:
|
||||||
params["auto_tagging"] = str(options.get("auto_tagging"))
|
params["auto_tagging"] = str(options.get("auto_tagging"))
|
||||||
if "access_control" in options:
|
if "access_control" in options:
|
||||||
params["access_control"] = utils.json_encode(utils.build_list_of_dicts(options.get("access_control")))
|
params["access_control"] = utils.json_encode(utils.build_list_of_dicts(options.get("access_control")))
|
||||||
|
if "asset_folder" in options:
|
||||||
|
params["asset_folder"] = options.get("asset_folder")
|
||||||
|
if "display_name" in options:
|
||||||
|
params["display_name"] = options.get("display_name")
|
||||||
|
if "unique_display_name" in options:
|
||||||
|
params["unique_display_name"] = options.get("unique_display_name")
|
||||||
|
|
||||||
return call_api("post", uri, params, **options)
|
return call_api("post", uri, params, **options)
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,9 @@ def generate(url=None, acl=None, start_time=None, duration=None,
|
||||||
token_parts.append("st=%d" % start_time)
|
token_parts.append("st=%d" % start_time)
|
||||||
token_parts.append("exp=%d" % expiration)
|
token_parts.append("exp=%d" % expiration)
|
||||||
if acl is not None:
|
if acl is not None:
|
||||||
token_parts.append("acl=%s" % _escape_to_lower(acl))
|
acl_list = acl if type(acl) is list else [acl]
|
||||||
|
acl_list = [_escape_to_lower(a) for a in acl_list]
|
||||||
|
token_parts.append("acl=%s" % "!".join(acl_list))
|
||||||
to_sign = list(token_parts)
|
to_sign = list(token_parts)
|
||||||
if url is not None and acl is None:
|
if url is not None and acl is None:
|
||||||
to_sign.append("url=%s" % _escape_to_lower(url))
|
to_sign.append("url=%s" % _escape_to_lower(url))
|
||||||
|
|
|
@ -106,7 +106,7 @@ class CloudinaryField(models.Field):
|
||||||
value = super(CloudinaryField, self).pre_save(model_instance, add)
|
value = super(CloudinaryField, self).pre_save(model_instance, add)
|
||||||
if isinstance(value, UploadedFile):
|
if isinstance(value, UploadedFile):
|
||||||
options = {"type": self.type, "resource_type": self.resource_type}
|
options = {"type": self.type, "resource_type": self.resource_type}
|
||||||
options.update(self.options)
|
options.update({key: val(model_instance) if callable(val) else val for key, val in self.options.items()})
|
||||||
if hasattr(value, 'seekable') and value.seekable():
|
if hasattr(value, 'seekable') and value.seekable():
|
||||||
value.seek(0)
|
value.seek(0)
|
||||||
instance_value = uploader.upload_resource(value, **options)
|
instance_value = uploader.upload_resource(value, **options)
|
||||||
|
|
|
@ -65,7 +65,7 @@ def create_sub_account(name, cloud_name=None, custom_attributes=None, enabled=No
|
||||||
"cloud_name": cloud_name,
|
"cloud_name": cloud_name,
|
||||||
"custom_attributes": custom_attributes,
|
"custom_attributes": custom_attributes,
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"base_account": base_account}
|
"base_sub_account_id": base_account}
|
||||||
return _call_account_api("POST", uri, params=params, **options)
|
return _call_account_api("POST", uri, params=params, **options)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1093,11 +1093,11 @@ var slice = [].slice,
|
||||||
*
|
*
|
||||||
* If the parameter is an object,
|
* If the parameter is an object,
|
||||||
* @param {(string|Object)} param - the video codec as either a String or a Hash
|
* @param {(string|Object)} param - the video codec as either a String or a Hash
|
||||||
* @return {string} the video codec string in the format codec:profile:level
|
* @return {string} the video codec string in the format codec:profile:level:b_frames
|
||||||
* @example
|
* @example
|
||||||
* vc_[ :profile : [level]]
|
* vc_[ :profile : [level : [b_frames]]]
|
||||||
* or
|
* or
|
||||||
{ codec: 'h264', profile: 'basic', level: '3.1' }
|
{ codec: 'h264', profile: 'basic', level: '3.1', b_frames: false }
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -1112,6 +1112,9 @@ var slice = [].slice,
|
||||||
video += ":" + param['profile'];
|
video += ":" + param['profile'];
|
||||||
if ('level' in param) {
|
if ('level' in param) {
|
||||||
video += ":" + param['level'];
|
video += ":" + param['level'];
|
||||||
|
if ('b_frames' in param && param['b_frames'] === false) {
|
||||||
|
video += ":bframes_no";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<script src="{% static "js/jquery.ui.widget.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.ui.widget.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.iframe-transport.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.iframe-transport.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.fileupload.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.fileupload.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.cloudinary.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.cloudinary.js" %}" type="text/javascript"></script>
|
||||||
|
|
||||||
{% if processing %}
|
{% if processing %}
|
||||||
<script src="{% static "js/load-image.all.min.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/load-image.all.min.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/canvas-to-blob.min.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/canvas-to-blob.min.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.fileupload-process.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.fileupload-process.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.fileupload-image.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.fileupload-image.js" %}" type="text/javascript"></script>
|
||||||
<script src="{% static "js/jquery.fileupload-validate.js" %}" type="text/javascript"></script>
|
<script src="{% static "cloudinary/js/jquery.fileupload-validate.js" %}" type="text/javascript"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -94,6 +94,8 @@ __SIMPLE_UPLOAD_PARAMS = [
|
||||||
"proxy",
|
"proxy",
|
||||||
"folder",
|
"folder",
|
||||||
"asset_folder",
|
"asset_folder",
|
||||||
|
"use_asset_folder_as_public_id_prefix",
|
||||||
|
"unique_display_name",
|
||||||
"overwrite",
|
"overwrite",
|
||||||
"moderation",
|
"moderation",
|
||||||
"raw_convert",
|
"raw_convert",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import io
|
import io
|
||||||
import keyword
|
import keyword
|
||||||
|
@ -6,20 +8,17 @@ import sys
|
||||||
import tokenize
|
import tokenize
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
# this is a performance hack. see https://bugs.python.org/issue43014
|
# this is a performance hack. see https://bugs.python.org/issue43014
|
||||||
if (
|
if ( # pragma: no branch
|
||||||
sys.version_info < (3, 10) and
|
sys.version_info < (3, 10) and
|
||||||
callable(getattr(tokenize, '_compile', None))
|
callable(getattr(tokenize, '_compile', None))
|
||||||
): # pragma: no cover (<py310)
|
): # pragma: <3.10 cover
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
tokenize._compile = lru_cache()(tokenize._compile) # type: ignore
|
tokenize._compile = lru_cache()(tokenize._compile)
|
||||||
|
|
||||||
ESCAPED_NL = 'ESCAPED_NL'
|
ESCAPED_NL = 'ESCAPED_NL'
|
||||||
UNIMPORTANT_WS = 'UNIMPORTANT_WS'
|
UNIMPORTANT_WS = 'UNIMPORTANT_WS'
|
||||||
|
@ -27,15 +26,15 @@ NON_CODING_TOKENS = frozenset(('COMMENT', ESCAPED_NL, 'NL', UNIMPORTANT_WS))
|
||||||
|
|
||||||
|
|
||||||
class Offset(NamedTuple):
|
class Offset(NamedTuple):
|
||||||
line: Optional[int] = None
|
line: int | None = None
|
||||||
utf8_byte_offset: Optional[int] = None
|
utf8_byte_offset: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class Token(NamedTuple):
|
class Token(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
src: str
|
src: str
|
||||||
line: Optional[int] = None
|
line: int | None = None
|
||||||
utf8_byte_offset: Optional[int] = None
|
utf8_byte_offset: int | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset(self) -> Offset:
|
def offset(self) -> Offset:
|
||||||
|
@ -43,11 +42,10 @@ class Token(NamedTuple):
|
||||||
|
|
||||||
|
|
||||||
_string_re = re.compile('^([^\'"]*)(.*)$', re.DOTALL)
|
_string_re = re.compile('^([^\'"]*)(.*)$', re.DOTALL)
|
||||||
_string_prefixes = frozenset('bfru')
|
|
||||||
_escaped_nl_re = re.compile(r'\\(\n|\r\n|\r)')
|
_escaped_nl_re = re.compile(r'\\(\n|\r\n|\r)')
|
||||||
|
|
||||||
|
|
||||||
def _re_partition(regex: Pattern[str], s: str) -> Tuple[str, str, str]:
|
def _re_partition(regex: Pattern[str], s: str) -> tuple[str, str, str]:
|
||||||
match = regex.search(s)
|
match = regex.search(s)
|
||||||
if match:
|
if match:
|
||||||
return s[:match.start()], s[slice(*match.span())], s[match.end():]
|
return s[:match.start()], s[slice(*match.span())], s[match.end():]
|
||||||
|
@ -55,7 +53,7 @@ def _re_partition(regex: Pattern[str], s: str) -> Tuple[str, str, str]:
|
||||||
return (s, '', '')
|
return (s, '', '')
|
||||||
|
|
||||||
|
|
||||||
def src_to_tokens(src: str) -> List[Token]:
|
def src_to_tokens(src: str) -> list[Token]:
|
||||||
tokenize_target = io.StringIO(src)
|
tokenize_target = io.StringIO(src)
|
||||||
lines = ('',) + tuple(tokenize_target)
|
lines = ('',) + tuple(tokenize_target)
|
||||||
|
|
||||||
|
@ -98,33 +96,7 @@ def src_to_tokens(src: str) -> List[Token]:
|
||||||
end_offset += len(newtok.encode())
|
end_offset += len(newtok.encode())
|
||||||
|
|
||||||
tok_name = tokenize.tok_name[tok_type]
|
tok_name = tokenize.tok_name[tok_type]
|
||||||
# when a string prefix is not recognized, the tokenizer produces a
|
tokens.append(Token(tok_name, tok_text, sline, end_offset))
|
||||||
# NAME token followed by a STRING token
|
|
||||||
if (
|
|
||||||
tok_name == 'STRING' and
|
|
||||||
tokens and
|
|
||||||
tokens[-1].name == 'NAME' and
|
|
||||||
frozenset(tokens[-1].src.lower()) <= _string_prefixes
|
|
||||||
):
|
|
||||||
newsrc = tokens[-1].src + tok_text
|
|
||||||
tokens[-1] = tokens[-1]._replace(src=newsrc, name=tok_name)
|
|
||||||
# produce octal literals as a single token in python 3 as well
|
|
||||||
elif (
|
|
||||||
tok_name == 'NUMBER' and
|
|
||||||
tokens and
|
|
||||||
tokens[-1].name == 'NUMBER'
|
|
||||||
):
|
|
||||||
tokens[-1] = tokens[-1]._replace(src=tokens[-1].src + tok_text)
|
|
||||||
# produce long literals as a single token in python 3 as well
|
|
||||||
elif (
|
|
||||||
tok_name == 'NAME' and
|
|
||||||
tok_text.lower() == 'l' and
|
|
||||||
tokens and
|
|
||||||
tokens[-1].name == 'NUMBER'
|
|
||||||
):
|
|
||||||
tokens[-1] = tokens[-1]._replace(src=tokens[-1].src + tok_text)
|
|
||||||
else:
|
|
||||||
tokens.append(Token(tok_name, tok_text, sline, end_offset))
|
|
||||||
last_line, last_col = eline, ecol
|
last_line, last_col = eline, ecol
|
||||||
if sline != eline:
|
if sline != eline:
|
||||||
end_offset = len(lines[last_line][:last_col].encode())
|
end_offset = len(lines[last_line][:last_col].encode())
|
||||||
|
@ -140,19 +112,19 @@ def tokens_to_src(tokens: Iterable[Token]) -> str:
|
||||||
|
|
||||||
def reversed_enumerate(
|
def reversed_enumerate(
|
||||||
tokens: Sequence[Token],
|
tokens: Sequence[Token],
|
||||||
) -> Generator[Tuple[int, Token], None, None]:
|
) -> Generator[tuple[int, Token], None, None]:
|
||||||
for i in reversed(range(len(tokens))):
|
for i in reversed(range(len(tokens))):
|
||||||
yield i, tokens[i]
|
yield i, tokens[i]
|
||||||
|
|
||||||
|
|
||||||
def parse_string_literal(src: str) -> Tuple[str, str]:
|
def parse_string_literal(src: str) -> tuple[str, str]:
|
||||||
"""parse a string literal's source into (prefix, string)"""
|
"""parse a string literal's source into (prefix, string)"""
|
||||||
match = _string_re.match(src)
|
match = _string_re.match(src)
|
||||||
assert match is not None
|
assert match is not None
|
||||||
return match.group(1), match.group(2)
|
return match.group(1), match.group(2)
|
||||||
|
|
||||||
|
|
||||||
def rfind_string_parts(tokens: Sequence[Token], i: int) -> Tuple[int, ...]:
|
def rfind_string_parts(tokens: Sequence[Token], i: int) -> tuple[int, ...]:
|
||||||
"""find the indicies of the string parts of a (joined) string literal
|
"""find the indicies of the string parts of a (joined) string literal
|
||||||
|
|
||||||
- `i` should start at the end of the string literal
|
- `i` should start at the end of the string literal
|
||||||
|
@ -195,7 +167,7 @@ def rfind_string_parts(tokens: Sequence[Token], i: int) -> Tuple[int, ...]:
|
||||||
return tuple(reversed(ret))
|
return tuple(reversed(ret))
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('filename')
|
parser.add_argument('filename')
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
@ -210,4 +182,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
apscheduler==3.9.1
|
apscheduler==3.9.1.post1
|
||||||
importlib-metadata==5.0.0
|
importlib-metadata==5.0.0
|
||||||
importlib-resources==5.10.0
|
importlib-resources==5.10.0
|
||||||
pyinstaller==5.6.2
|
pyinstaller==5.6.2
|
||||||
pyopenssl==22.0.0
|
pyopenssl==22.1.0
|
||||||
pycryptodomex==3.15.0
|
pycryptodomex==3.15.0
|
||||||
|
|
||||||
pyobjc-framework-Cocoa==8.5; platform_system == "Darwin"
|
pyobjc-framework-Cocoa==9.0; platform_system == "Darwin"
|
||||||
pyobjc-core==9.0; platform_system == "Darwin"
|
pyobjc-core==9.0; platform_system == "Darwin"
|
||||||
|
|
||||||
pywin32==304; platform_system == "Windows"
|
pywin32==305; platform_system == "Windows"
|
||||||
|
|
|
@ -126,6 +126,7 @@ _CONFIG_DEFINITIONS = {
|
||||||
'HTTPS_KEY': (str, 'General', ''),
|
'HTTPS_KEY': (str, 'General', ''),
|
||||||
'HTTPS_DOMAIN': (str, 'General', 'localhost'),
|
'HTTPS_DOMAIN': (str, 'General', 'localhost'),
|
||||||
'HTTPS_IP': (str, 'General', '127.0.0.1'),
|
'HTTPS_IP': (str, 'General', '127.0.0.1'),
|
||||||
|
'HTTPS_MIN_TLS_VERSION': (str, 'Advanced', 'TLSv1.2'),
|
||||||
'HTTP_BASIC_AUTH': (int, 'General', 0),
|
'HTTP_BASIC_AUTH': (int, 'General', 0),
|
||||||
'HTTP_ENVIRONMENT': (str, 'General', 'production'),
|
'HTTP_ENVIRONMENT': (str, 'General', 'production'),
|
||||||
'HTTP_HASH_PASSWORD': (int, 'General', 1),
|
'HTTP_HASH_PASSWORD': (int, 'General', 1),
|
||||||
|
|
|
@ -102,7 +102,7 @@ class Export(object):
|
||||||
METADATA_LEVELS = (0, 1, 2, 3, 9)
|
METADATA_LEVELS = (0, 1, 2, 3, 9)
|
||||||
MEDIA_INFO_LEVELS = (0, 1, 2, 3, 9)
|
MEDIA_INFO_LEVELS = (0, 1, 2, 3, 9)
|
||||||
IMAGE_LEVELS = (0, 1, 2, 9)
|
IMAGE_LEVELS = (0, 1, 2, 9)
|
||||||
FILE_FORMATS = ('csv', 'json', 'xml', 'm3u8')
|
FILE_FORMATS = ('csv', 'json', 'xml', 'm3u')
|
||||||
EXPORT_TYPES = ('all', 'collection', 'playlist')
|
EXPORT_TYPES = ('all', 'collection', 'playlist')
|
||||||
|
|
||||||
def __init__(self, section_id=None, user_id=None, rating_key=None, file_format='csv',
|
def __init__(self, section_id=None, user_id=None, rating_key=None, file_format='csv',
|
||||||
|
@ -141,8 +141,8 @@ class Export(object):
|
||||||
self.exported_items = 0
|
self.exported_items = 0
|
||||||
self.success = False
|
self.success = False
|
||||||
|
|
||||||
# Reset export options for m3u8
|
# Reset export options for m3u
|
||||||
if self.file_format == 'm3u8':
|
if self.file_format == 'm3u':
|
||||||
self.metadata_level = 1
|
self.metadata_level = 1
|
||||||
self.media_info_level = 1
|
self.media_info_level = 1
|
||||||
self.thumb_level = 0
|
self.thumb_level = 0
|
||||||
|
@ -1960,10 +1960,10 @@ class Export(object):
|
||||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
outfile.write(xml_data)
|
outfile.write(xml_data)
|
||||||
|
|
||||||
elif self.file_format == 'm3u8':
|
elif self.file_format == 'm3u':
|
||||||
m3u8_data = self.data_to_m3u8(result, obj)
|
m3u_data = self.data_to_m3u(result, obj)
|
||||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
outfile.write(m3u8_data)
|
outfile.write(m3u_data)
|
||||||
|
|
||||||
self.file_size += os.path.getsize(filepath)
|
self.file_size += os.path.getsize(filepath)
|
||||||
|
|
||||||
|
@ -2119,36 +2119,36 @@ class Export(object):
|
||||||
|
|
||||||
return helpers.dict_to_xml(xml_metadata, root_node='export', indent=4)
|
return helpers.dict_to_xml(xml_metadata, root_node='export', indent=4)
|
||||||
|
|
||||||
def data_to_m3u8(self, data, obj):
|
def data_to_m3u(self, data, obj):
|
||||||
items = self._get_m3u8_items(data)
|
items = self._get_m3u_items(data)
|
||||||
|
|
||||||
m3u8_metadata = {'title': obj.title, 'type': obj.media_type}
|
m3u_metadata = {'title': obj.title, 'type': obj.media_type}
|
||||||
if obj.rating_key:
|
if obj.rating_key:
|
||||||
m3u8_metadata['ratingKey'] = obj.rating_key
|
m3u_metadata['ratingKey'] = obj.rating_key
|
||||||
if obj.user_id:
|
if obj.user_id:
|
||||||
m3u8_metadata['userID'] = obj.user_id
|
m3u_metadata['userID'] = obj.user_id
|
||||||
if obj.section_id:
|
if obj.section_id:
|
||||||
m3u8_metadata['sectionID'] = obj.section_id
|
m3u_metadata['sectionID'] = obj.section_id
|
||||||
|
|
||||||
m3u8 = '#EXTM3U\n'
|
m3u = '#EXTM3U\n'
|
||||||
m3u8 += '# Playlist: {title}\n# {metadata}\n\n'.format(title=obj.title, metadata=json.dumps(m3u8_metadata))
|
m3u += '# Playlist: {title}\n# {metadata}\n\n'.format(title=obj.title, metadata=json.dumps(m3u_metadata))
|
||||||
m3u8_item_template = '# {metadata}\n#EXTINF:{duration},{title}\n{location}\n'
|
m3u_item_template = '# {metadata}\n#EXTINF:{duration},{title}\n{location}\n'
|
||||||
m3u8_items = []
|
m3u_items = []
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
m3u8_values = {
|
m3u_values = {
|
||||||
'duration': item.pop('duration'),
|
'duration': item.pop('duration'),
|
||||||
'title': item.pop('title'),
|
'title': item.pop('title'),
|
||||||
'location': item.pop('location'),
|
'location': item.pop('location'),
|
||||||
'metadata': json.dumps(item)
|
'metadata': json.dumps(item)
|
||||||
}
|
}
|
||||||
m3u8_items.append(m3u8_item_template.format(**m3u8_values))
|
m3u_items.append(m3u_item_template.format(**m3u_values))
|
||||||
|
|
||||||
m3u8 = m3u8 + '\n'.join(m3u8_items)
|
m3u = m3u + '\n'.join(m3u_items)
|
||||||
|
|
||||||
return m3u8
|
return m3u
|
||||||
|
|
||||||
def _get_m3u8_items(self, data):
|
def _get_m3u_items(self, data):
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
|
@ -2167,7 +2167,7 @@ class Export(object):
|
||||||
items.append(metadata)
|
items.append(metadata)
|
||||||
|
|
||||||
for child_media_type in self.CHILD_MEDIA_TYPES[d['type']]:
|
for child_media_type in self.CHILD_MEDIA_TYPES[d['type']]:
|
||||||
child_locations = self._get_m3u8_items(d[self.PLURAL_MEDIA_TYPES[child_media_type]])
|
child_locations = self._get_m3u_items(d[self.PLURAL_MEDIA_TYPES[child_media_type]])
|
||||||
items.extend(child_locations)
|
items.extend(child_locations)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
|
@ -7003,7 +7003,7 @@ class WebInterface(object):
|
||||||
rating_key (int): The rating key of the media item to export
|
rating_key (int): The rating key of the media item to export
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
file_format (str): csv (default), json, xml, or m3u8
|
file_format (str): csv (default), json, xml, or m3u
|
||||||
metadata_level (int): The level of metadata to export (default 1)
|
metadata_level (int): The level of metadata to export (default 1)
|
||||||
media_info_level (int): The level of media info to export (default 1)
|
media_info_level (int): The level of media info to export (default 1)
|
||||||
thumb_level (int): The level of poster/cover images to export (default 0)
|
thumb_level (int): The level of poster/cover images to export (default 0)
|
||||||
|
@ -7084,7 +7084,7 @@ class WebInterface(object):
|
||||||
elif result['file_format'] == 'xml':
|
elif result['file_format'] == 'xml':
|
||||||
return serve_file(filepath, name=result['filename'], content_type='application/xml;charset=UTF-8')
|
return serve_file(filepath, name=result['filename'], content_type='application/xml;charset=UTF-8')
|
||||||
|
|
||||||
elif result['file_format'] == 'm3u8':
|
elif result['file_format'] == 'm3u':
|
||||||
return serve_file(filepath, name=result['filename'], content_type='text/plain;charset=UTF-8')
|
return serve_file(filepath, name=result['filename'], content_type='text/plain;charset=UTF-8')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -46,6 +46,7 @@ def start():
|
||||||
'https_cert': plexpy.CONFIG.HTTPS_CERT,
|
'https_cert': plexpy.CONFIG.HTTPS_CERT,
|
||||||
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
|
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
|
||||||
'https_key': plexpy.CONFIG.HTTPS_KEY,
|
'https_key': plexpy.CONFIG.HTTPS_KEY,
|
||||||
|
'https_min_tls_version': plexpy.CONFIG.HTTPS_MIN_TLS_VERSION,
|
||||||
'http_username': plexpy.CONFIG.HTTP_USERNAME,
|
'http_username': plexpy.CONFIG.HTTP_USERNAME,
|
||||||
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
|
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
|
||||||
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
|
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
|
||||||
|
@ -106,7 +107,11 @@ def initialize(options):
|
||||||
purpose=ssl.Purpose.CLIENT_AUTH,
|
purpose=ssl.Purpose.CLIENT_AUTH,
|
||||||
cafile=https_cert_chain
|
cafile=https_cert_chain
|
||||||
)
|
)
|
||||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
||||||
|
min_tls_version = options['https_min_tls_version'].replace('.', '_')
|
||||||
|
context.minimum_version = getattr(ssl.TLSVersion, min_tls_version, ssl.TLSVersion.TLSv1_2)
|
||||||
|
logger.debug("Tautulli WebStart :: Web server minimum TLS version set to %s.", context.minimum_version.name)
|
||||||
|
|
||||||
context.load_cert_chain(https_cert, https_key)
|
context.load_cert_chain(https_cert, https_key)
|
||||||
|
|
||||||
options_dict['server.ssl_context'] = context
|
options_dict['server.ssl_context'] = context
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
appdirs==1.4.4
|
appdirs==1.4.4
|
||||||
apscheduler==3.9.1
|
apscheduler==3.9.1.post1
|
||||||
arrow==1.2.3
|
arrow==1.2.3
|
||||||
backports.csv==1.0.7
|
backports.csv==1.0.7
|
||||||
backports.functools-lru-cache==1.6.4
|
backports.functools-lru-cache==1.6.4
|
||||||
|
@ -9,7 +9,7 @@ bleach==5.0.1
|
||||||
certifi==2022.9.24
|
certifi==2022.9.24
|
||||||
cheroot==8.6.0
|
cheroot==8.6.0
|
||||||
cherrypy==18.8.0
|
cherrypy==18.8.0
|
||||||
cloudinary==1.29.0
|
cloudinary==1.30.0
|
||||||
distro==1.8.0
|
distro==1.8.0
|
||||||
dnspython==2.2.1
|
dnspython==2.2.1
|
||||||
facebook-sdk==3.1.0
|
facebook-sdk==3.1.0
|
||||||
|
@ -42,7 +42,7 @@ simplejson==3.17.6
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
soupsieve==2.3.2.post1
|
soupsieve==2.3.2.post1
|
||||||
tempora==5.0.2
|
tempora==5.0.2
|
||||||
tokenize-rt==4.2.1
|
tokenize-rt==5.0.0
|
||||||
tzdata==2022.6
|
tzdata==2022.6
|
||||||
tzlocal==4.2
|
tzlocal==4.2
|
||||||
urllib3==1.26.12
|
urllib3==1.26.12
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue