Merge branch 'nightly' into dependabot/pip/nightly/pyopenssl-25.0.0

This commit is contained in:
JonnyWong16 2025-03-24 14:04:31 -07:00 committed by GitHub
commit 4b8d4627b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 381 additions and 65 deletions

View file

@ -3,7 +3,7 @@
## v2.15.1 (2025-01-11) ## v2.15.1 (2025-01-11)
* Activity: * Activity:
* Fix: Detection of HDR transcodes. (Thanks @chrisdecker08) (#2412, #2466) * Fix: Detection of HDR transcodes. (Thanks @cdecker08) (#2412, #2466)
* Newsletters: * Newsletters:
* Fix: Disable basic authentication for /newsletter and /image endpoints. (#2472) * Fix: Disable basic authentication for /newsletter and /image endpoints. (#2472)
* Exporter: * Exporter:

View file

@ -129,7 +129,7 @@ This is free software under the GPL v3 open source license. Feel free to do with
but any modification must be open sourced. A copy of the license is included. but any modification must be open sourced. A copy of the license is included.
This software includes Highsoft software libraries which you may freely distribute for This software includes Highsoft software libraries which you may freely distribute for
non-commercial use. Commerical users must licence this software, for more information visit non-commercial use. Commercial users must licence this software, for more information visit
https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution. https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.

View file

@ -129,7 +129,7 @@ def main():
if args.quiet: if args.quiet:
plexpy.QUIET = True plexpy.QUIET = True
# Do an intial setup of the logger. # Do an initial setup of the logger.
# Require verbose for pre-initilization to see critical errors # Require verbose for pre-initilization to see critical errors
logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True) logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)

View file

@ -4325,6 +4325,10 @@ a:hover .overlay-refresh-image:hover {
.stream-info tr:nth-child(even) td { .stream-info tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010); background-color: rgba(255,255,255,0.010);
} }
.stream-info td:nth-child(3),
.stream-info th:nth-child(3) {
width: 25px;
}
.number-input { .number-input {
margin: 0 !important; margin: 0 !important;
width: 55px !important; width: 55px !important;

View file

@ -74,6 +74,7 @@ DOCUMENTATION :: END
parent_href = page('info', data['parent_rating_key']) parent_href = page('info', data['parent_rating_key'])
grandparent_href = page('info', data['grandparent_rating_key']) grandparent_href = page('info', data['grandparent_rating_key'])
user_href = page('user', data['user_id']) if data['user_id'] else '#' user_href = page('user', data['user_id']) if data['user_id'] else '#'
library_href = page('library', data['section_id'])
season = short_season(data['parent_title']) season = short_season(data['parent_title'])
%> %>
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}" <div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
@ -463,21 +464,27 @@ DOCUMENTATION :: END
<div class="dashboard-activity-metadata-subtitle-container"> <div class="dashboard-activity-metadata-subtitle-container">
% if data['live']: % if data['live']:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Live TV"> <div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Live TV">
<i class="fa fa-fw fa-broadcast-tower"></i>&nbsp; <a href="${library_href}">
<i class="fa fa-fw fa-broadcast-tower"></i>
</a>&nbsp;
</div> </div>
% elif data['channel_stream'] == 0: % elif data['channel_stream'] == 0:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}"> <div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
<a href="${library_href}">
% if data['media_type'] == 'movie': % if data['media_type'] == 'movie':
<i class="fa fa-fw fa-film"></i>&nbsp; <i class="fa fa-fw fa-film"></i>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<i class="fa fa-fw fa-television"></i>&nbsp; <i class="fa fa-fw fa-television"></i>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<i class="fa fa-fw fa-music"></i>&nbsp; <i class="fa fa-fw fa-music"></i>
% elif data['media_type'] == 'photo': % elif data['media_type'] == 'photo':
<i class="fa fa-fw fa-picture-o"></i>&nbsp; <i class="fa fa-fw fa-picture-o"></i>
% elif data['media_type'] == 'clip': % elif data['media_type'] == 'clip':
<i class="fa fa-fw fa-video-camera"></i>&nbsp; <i class="fa fa-fw fa-video-camera"></i>
% else:
<i class="fa fa-fw fa-question-circle"></i>
% endif % endif
</a>&nbsp;
</div> </div>
% else: % else:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Channel"> <div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Channel">

View file

@ -68,14 +68,14 @@ DOCUMENTATION :: END
<table class="stream-info" style="margin-top: 0;"> <table class="stream-info" style="margin-top: 0;">
<thead> <thead>
<tr> <tr>
<th> <th></th>
</th>
<th class="heading">
Stream Details
</th>
<th class="heading"> <th class="heading">
Source Details Source Details
</th> </th>
<th><i class="fa fa-long-arrow-right"></i></th>
<th class="heading">
Stream Details
</th>
</tr> </tr>
</thead> </thead>
</table> </table>
@ -85,38 +85,46 @@ DOCUMENTATION :: END
<th> <th>
Media Media
</th> </th>
<th></th>
<th></th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
<td>${data['stream_bitrate']} ${'kbps' if data['stream_bitrate'] else ''}</td>
<td>${data['bitrate']} ${'kbps' if data['bitrate'] else ''}</td> <td>${data['bitrate']} ${'kbps' if data['bitrate'] else ''}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_bitrate']} ${'kbps' if data['stream_bitrate'] else ''}</td>
</tr> </tr>
% if data['media_type'] != 'track': % if data['media_type'] != 'track':
<tr> <tr>
<td>Resolution</td> <td>Resolution</td>
<td>${data['stream_video_full_resolution']}</td>
<td>${data['video_full_resolution']}</td> <td>${data['video_full_resolution']}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_full_resolution']}</td>
</tr> </tr>
% endif % endif
<tr> <tr>
<td>Quality</td> <td>Quality</td>
<td>${data['quality_profile']}</td>
<td>-</td> <td>-</td>
<td></td>
<td>${data['quality_profile']}</td>
</tr> </tr>
% if data['optimized_version'] == 1: % if data['optimized_version'] == 1:
<tr> <tr>
<td>Optimized Version</td> <td>Optimized Version</td>
<td>-</td>
<td>${data['optimized_version_profile']}<br>(${data['optimized_version_title']})</td> <td>${data['optimized_version_profile']}<br>(${data['optimized_version_title']})</td>
<td></td>
<td>-</td>
</tr> </tr>
% endif % endif
% if data['synced_version'] == 1: % if data['synced_version'] == 1:
<tr> <tr>
<td>Synced Version</td> <td>Synced Version</td>
<td>-</td>
<td>${data['synced_version_profile']}</td> <td>${data['synced_version_profile']}</td>
<td></td>
<td>-</td>
</tr> </tr>
% endif % endif
</tbody> </tbody>
@ -127,6 +135,8 @@ DOCUMENTATION :: END
<th> <th>
Container Container
</th> </th>
<th></th>
<th></th>
<th> <th>
${data['stream_container_decision']} ${data['stream_container_decision']}
</th> </th>
@ -135,8 +145,9 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Container</td> <td>Container</td>
<td>${data['stream_container'].upper()}</td>
<td>${data['container'].upper()}</td> <td>${data['container'].upper()}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_container'].upper()}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -147,6 +158,8 @@ DOCUMENTATION :: END
<th> <th>
Video Video
</th> </th>
<th></th>
<th></th>
<th> <th>
${data['stream_video_decision']} ${data['stream_video_decision']}
</th> </th>
@ -155,38 +168,45 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${data['stream_video_codec'].upper()} ${'(HW)' if data['transcode_hw_encoding'] else ''}</td>
<td>${data['video_codec'].upper()} ${'(HW)' if data['transcode_hw_decoding'] else ''}</td> <td>${data['video_codec'].upper()} ${'(HW)' if data['transcode_hw_decoding'] else ''}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_codec'].upper()} ${'(HW)' if data['transcode_hw_encoding'] else ''}</td>
</tr> </tr>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
<td>${data['stream_video_bitrate']} ${'kbps' if data['stream_video_bitrate'] else ''}</td>
<td>${data['video_bitrate']} ${'kbps' if data['video_bitrate'] else ''}</td> <td>${data['video_bitrate']} ${'kbps' if data['video_bitrate'] else ''}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_bitrate']} ${'kbps' if data['stream_video_bitrate'] else ''}</td>
</tr> </tr>
<tr> <tr>
<td>Width</td> <td>Width</td>
<td>${data['stream_video_width']}</td>
<td>${data['video_width']}</td> <td>${data['video_width']}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_width']}</td>
</tr> </tr>
<tr> <tr>
<td>Height</td> <td>Height</td>
<td>${data['stream_video_height']}</td>
<td>${data['video_height']}</td> <td>${data['video_height']}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_height']}</td>
</tr> </tr>
<tr> <tr>
<td>Framerate</td> <td>Framerate</td>
<td>${data['stream_video_framerate']}</td>
<td>${data['video_framerate']}</td> <td>${data['video_framerate']}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_framerate']}</td>
</tr> </tr>
<tr> <tr>
<td>Dynamic Range</td> <td>Dynamic Range</td>
<td>${data['stream_video_dynamic_range']}</td>
<td>${data['video_dynamic_range']}</td> <td>${data['video_dynamic_range']}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_video_dynamic_range']}</td>
</tr> </tr>
<tr> <tr>
<td>Aspect Ratio</td> <td>Aspect Ratio</td>
<td>-</td>
<td>${data['aspect_ratio']}</td> <td>${data['aspect_ratio']}</td>
<td></td>
<td>-</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -197,6 +217,8 @@ DOCUMENTATION :: END
<th> <th>
Audio Audio
</th> </th>
<th></th>
<th></th>
<th> <th>
${data['stream_audio_decision']} ${data['stream_audio_decision']}
</th> </th>
@ -205,23 +227,27 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())}</td>
<td>${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())}</td> <td>${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())}</td>
</tr> </tr>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
<td>${data['stream_audio_bitrate']} ${'kbps' if data['stream_audio_bitrate'] else ''}</td>
<td>${data['audio_bitrate']} ${'kbps' if data['audio_bitrate'] else ''}</td> <td>${data['audio_bitrate']} ${'kbps' if data['audio_bitrate'] else ''}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_audio_bitrate']} ${'kbps' if data['stream_audio_bitrate'] else ''}</td>
</tr> </tr>
<tr> <tr>
<td>Channels</td> <td>Channels</td>
<td>${data['stream_audio_channels']}</td>
<td>${data['audio_channels']}</td> <td>${data['audio_channels']}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_audio_channels']}</td>
</tr> </tr>
<tr> <tr>
<td>Language</td> <td>Language</td>
<td>-</td>
<td>${data['audio_language'] or 'Unknown'}</td> <td>${data['audio_language'] or 'Unknown'}</td>
<td></td>
<td>-</td>
</tr> </tr>
</tbody> </tbody>
@ -233,6 +259,8 @@ DOCUMENTATION :: END
<th> <th>
Subtitles Subtitles
</th> </th>
<th></th>
<th></th>
<th> <th>
${'direct play' if data['stream_subtitle_decision'] not in ('transcode', 'copy', 'burn') else data['stream_subtitle_decision']} ${'direct play' if data['stream_subtitle_decision'] not in ('transcode', 'copy', 'burn') else data['stream_subtitle_decision']}
</th> </th>
@ -241,19 +269,22 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${data['stream_subtitle_codec'].upper() or '-'}</td>
<td>${data['subtitle_codec'].upper()}</td> <td>${data['subtitle_codec'].upper()}</td>
<td><i class="fa fa-long-arrow-right"></i></td>
<td>${data['stream_subtitle_codec'].upper() or '-'}</td>
</tr> </tr>
<tr> <tr>
<td>Language</td> <td>Language</td>
<td>-</td>
<td>${data['subtitle_language'] or 'Unknown'}</td> <td>${data['subtitle_language'] or 'Unknown'}</td>
<td></td>
<td>-</td>
</tr> </tr>
% if data['subtitle_forced']: % if data['subtitle_forced']:
<tr> <tr>
<td>Forced</td> <td>Forced</td>
<td>-</td>
<td>${bool(data['subtitle_forced'])}</td> <td>${bool(data['subtitle_forced'])}</td>
<td></td>
<td>-</td>
</tr> </tr>
% endif % endif
</tbody> </tbody>

View file

@ -0,0 +1,255 @@
import re
import cherrypy
from cherrypy.lib import set_vary_header
import httpagentparser
CORS_ALLOW_METHODS = 'Access-Control-Allow-Methods'
CORS_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
CORS_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'
CORS_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
CORS_REQUEST_METHOD = 'Access-Control-Request-Method'
CORS_REQUEST_HEADERS = 'Access-Control-Request-Headers'
CORS_MAX_AGE = 'Access-Control-Max-Age'
CORS_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
PUBLIC_ORIGIN = '*'
def expose(allow_credentials=False, expose_headers=None, origins=None):
"""Adds CORS support to the resource.
If the resource is allowed to be exposed, the value of the
`Access-Control-Allow-Origin`_ header in the response will echo
the `Origin`_ request header, and `Origin` will be
appended to the `Vary`_ response header.
:param allow_credentials: Use credentials to make cookies work
(see `Access-Control-Allow-Credentials`_).
:type allow_credentials: bool
:param expose_headers: List of headers clients will be able to access
(see `Access-Control-Expose-Headers`_).
:type expose_headers: list or None
:param origins: List of allowed origins clients must reference.
:type origins: list or None
:returns: Whether the resource is being exposed.
:rtype: bool
- Configuration example:
.. code-block:: python
config = {
'/static': {
'tools.staticdir.on': True,
'cors.expose.on': True,
}
}
- Decorator example:
.. code-block:: python
@cherrypy_cors.tools.expose()
def DELETE(self):
self._delete()
"""
if _get_cors().expose(allow_credentials, expose_headers, origins):
_safe_caching_headers()
return True
return False
def expose_public(expose_headers=None):
"""Adds CORS support to the resource from any origin.
If the resource is allowed to be exposed, the value of the
`Access-Control-Allow-Origin`_ header in the response will be `*`.
:param expose_headers: List of headers clients will be able to access
(see `Access-Control-Expose-Headers`_).
:type expose_headers: list or None
:rtype: None
"""
_get_cors().expose_public(expose_headers)
def preflight(
allowed_methods,
allowed_headers=None,
allow_credentials=False,
max_age=None,
origins=None,
):
"""Adds CORS `preflight`_ support to a `HTTP OPTIONS` request.
:param allowed_methods: List of supported `HTTP` methods
(see `Access-Control-Allow-Methods`_).
:type allowed_methods: list or None
:param allowed_headers: List of supported `HTTP` headers
(see `Access-Control-Allow-Headers`_).
:type allowed_headers: list or None
:param allow_credentials: Use credentials to make cookies work
(see `Access-Control-Allow-Credentials`_).
:type allow_credentials: bool
:param max_age: Seconds to cache the preflight request
(see `Access-Control-Max-Age`_).
:type max_age: int
:param origins: List of allowed origins clients must reference.
:type origins: list or None
:returns: Whether the preflight is allowed.
:rtype: bool
- Used as a decorator with the `Method Dispatcher`_
.. code-block:: python
@cherrypy_cors.tools.preflight(
allowed_methods=["GET", "DELETE", "PUT"])
def OPTIONS(self):
pass
- Function call with the `Object Dispatcher`_
.. code-block:: python
@cherrypy.expose
@cherrypy.tools.allow(
methods=["GET", "DELETE", "PUT", "OPTIONS"])
def thing(self):
if cherrypy.request.method == "OPTIONS":
cherrypy_cors.preflight(
allowed_methods=["GET", "DELETE", "PUT"])
else:
self._do_other_things()
"""
if _get_cors().preflight(
allowed_methods, allowed_headers, allow_credentials, max_age, origins
):
_safe_caching_headers()
return True
return False
def install():
"""Install the toolbox such that it's available in all applications."""
cherrypy._cptree.Application.toolboxes.update(cors=tools)
class CORS:
"""A generic CORS handler."""
def __init__(self, req_headers, resp_headers):
self.req_headers = req_headers
self.resp_headers = resp_headers
def expose(self, allow_credentials, expose_headers, origins):
if self._is_valid_origin(origins):
self._add_origin_and_credentials_headers(allow_credentials)
self._add_expose_headers(expose_headers)
return True
return False
def expose_public(self, expose_headers):
self._add_public_origin()
self._add_expose_headers(expose_headers)
def preflight(
self, allowed_methods, allowed_headers, allow_credentials, max_age, origins
):
if self._is_valid_preflight_request(allowed_headers, allowed_methods, origins):
self._add_origin_and_credentials_headers(allow_credentials)
self._add_prefligt_headers(allowed_methods, max_age)
return True
return False
@property
def origin(self):
return self.req_headers.get('Origin')
def _is_valid_origin(self, origins):
if origins is None:
origins = [self.origin]
origins = map(self._make_regex, origins)
return self.origin is not None and any(
origin.match(self.origin) for origin in origins
)
@staticmethod
def _make_regex(pattern):
if isinstance(pattern, str):
pattern = re.compile(re.escape(pattern) + '$')
return pattern
def _add_origin_and_credentials_headers(self, allow_credentials):
self.resp_headers[CORS_ALLOW_ORIGIN] = self.origin
if allow_credentials:
self.resp_headers[CORS_ALLOW_CREDENTIALS] = 'true'
def _add_public_origin(self):
self.resp_headers[CORS_ALLOW_ORIGIN] = PUBLIC_ORIGIN
def _add_expose_headers(self, expose_headers):
if expose_headers:
self.resp_headers[CORS_EXPOSE_HEADERS] = expose_headers
@property
def requested_method(self):
return self.req_headers.get(CORS_REQUEST_METHOD)
@property
def requested_headers(self):
return self.req_headers.get(CORS_REQUEST_HEADERS)
def _has_valid_method(self, allowed_methods):
return self.requested_method and self.requested_method in allowed_methods
def _valid_headers(self, allowed_headers):
if self.requested_headers and allowed_headers:
for header in self.requested_headers.split(','):
if header.strip() not in allowed_headers:
return False
return True
def _is_valid_preflight_request(self, allowed_headers, allowed_methods, origins):
return (
self._is_valid_origin(origins)
and self._has_valid_method(allowed_methods)
and self._valid_headers(allowed_headers)
)
def _add_prefligt_headers(self, allowed_methods, max_age):
rh = self.resp_headers
rh[CORS_ALLOW_METHODS] = ', '.join(allowed_methods)
if max_age:
rh[CORS_MAX_AGE] = max_age
if self.requested_headers:
rh[CORS_ALLOW_HEADERS] = self.requested_headers
def _get_cors():
return CORS(cherrypy.serving.request.headers, cherrypy.serving.response.headers)
def _safe_caching_headers():
"""Adds `Origin`_ to the `Vary`_ header to ensure caching works properly.
Except in IE because it will disable caching completely. The caching
strategy in that case is out of the scope of this library.
https://blogs.msdn.microsoft.com/ieinternals/2009/06/17/vary-with-care/
"""
uah = cherrypy.serving.request.headers.get('User-Agent', '')
ua = httpagentparser.detect(uah)
IE = 'Microsoft Internet Explorer'
if ua.get('browser', {}).get('name') != IE:
set_vary_header(cherrypy.serving.response, "Origin")
tools = cherrypy._cptools.Toolbox("cors")
tools.expose = cherrypy.Tool('before_handler', expose)
tools.expose_public = cherrypy.Tool('before_handler', expose_public)
tools.preflight = cherrypy.Tool('before_handler', preflight)

View file

@ -2,7 +2,7 @@ apscheduler==3.10.1
cryptography==43.0.3 cryptography==43.0.3
importlib-metadata==8.5.0 importlib-metadata==8.5.0
importlib-resources==6.4.5 importlib-resources==6.4.5
pyinstaller==6.11.1 pyinstaller==6.10.0
pyopenssl==25.0.0 pyopenssl==25.0.0
pyobjc-core==10.3.1; platform_system == "Darwin" pyobjc-core==10.3.1; platform_system == "Darwin"

View file

@ -314,6 +314,10 @@ class ActivityHandler(object):
if self.metadata: if self.metadata:
this_guid = self.metadata['guid'] this_guid = self.metadata['guid']
# Check for stream offset notifications
self.check_markers()
self.check_watched()
# Make sure the same item is being played # Make sure the same item is being played
if (self.rating_key == last_rating_key if (self.rating_key == last_rating_key
or self.rating_key == last_rating_key_websocket or self.rating_key == last_rating_key_websocket
@ -354,10 +358,6 @@ class ActivityHandler(object):
self.on_stop(force_stop=True) self.on_stop(force_stop=True)
self.on_start() self.on_start()
# Check for stream offset notifications
self.check_markers()
self.check_watched()
def check_markers(self): def check_markers(self):
# Monitor if the stream has reached the intro or credit marker offsets # Monitor if the stream has reached the intro or credit marker offsets
self.get_metadata() self.get_metadata()

View file

@ -173,7 +173,7 @@ def check_active_sessions(ws_request=False):
row_id = monitor_process.write_session_history(session=stream) row_id = monitor_process.write_session_history(session=stream)
if row_id: if row_id:
# If session is written to the databaase successfully, remove the session from the session table # If session is written to the database successfully, remove the session from the session table
logger.debug("Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue" logger.debug("Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
% (stream['session_key'], stream['rating_key'])) % (stream['session_key'], stream['rating_key']))
monitor_process.delete_session(row_id=row_id) monitor_process.delete_session(row_id=row_id)

View file

@ -476,6 +476,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the stream.'}, {'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the stream.'},
{'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language', 'description': 'The audio language of the stream.'}, {'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language', 'description': 'The audio language of the stream.'},
{'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code', 'description': 'The audio language code of the stream.'}, {'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code', 'description': 'The audio language code of the stream.'},
{'name': 'Stream Audio Profile', 'type': 'str', 'value': 'stream_audio_profile', 'description': 'The audio profile of the stream.'},
{'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec', 'description': 'The subtitle codec of the stream.'}, {'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec', 'description': 'The subtitle codec of the stream.'},
{'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container', 'description': 'The subtitle container of the stream.'}, {'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container', 'description': 'The subtitle container of the stream.'},
{'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format', 'description': 'The subtitle format of the stream.'}, {'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format', 'description': 'The subtitle format of the stream.'},
@ -606,6 +607,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the original media.'}, {'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the original media.'},
{'name': 'Audio Language', 'type': 'str', 'value': 'audio_language', 'description': 'The audio language of the original media.'}, {'name': 'Audio Language', 'type': 'str', 'value': 'audio_language', 'description': 'The audio language of the original media.'},
{'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code', 'description': 'The audio language code of the original media.'}, {'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code', 'description': 'The audio language code of the original media.'},
{'name': 'Audio Profile', 'type': 'str', 'value': 'audio_profile', 'description': 'The audio profile of the original media.'},
{'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec', 'description': 'The subtitle codec of the original media.'}, {'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec', 'description': 'The subtitle codec of the original media.'},
{'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container', 'description': 'The subtitle container of the original media.'}, {'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container', 'description': 'The subtitle container of the original media.'},
{'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format', 'description': 'The subtitle format of the original media.'}, {'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format', 'description': 'The subtitle format of the original media.'},

View file

@ -568,7 +568,7 @@ class Config(object):
def _upgrade(self): def _upgrade(self):
""" """
Upgrades config file from previous verisions and bumps up config version Upgrades config file from previous versions and bumps up config version
""" """
if self.CONFIG_VERSION == 0: if self.CONFIG_VERSION == 0:
self.CONFIG_VERSION = 1 self.CONFIG_VERSION = 1

View file

@ -283,7 +283,7 @@ def extract_columns(columns=None, match_columns=None):
columns_string = columns_string.rstrip(', ') columns_string = columns_string.rstrip(', ')
# We return a dict of the column params # We return a dict of the column params
# column_string is a comma seperated list of the exact column variables received. # column_string is a comma separated list of the exact column variables received.
# column_literal is the text before the "as" if we have an "as". Usually a function. # column_literal is the text before the "as" if we have an "as". Usually a function.
# column_named is the text after the "as", if we have an "as". Any table prefix is also stripped off. # column_named is the text after the "as", if we have an "as". Any table prefix is also stripped off.
# We use this to match with columns received from the Datatables request. # We use this to match with columns received from the Datatables request.

View file

@ -132,7 +132,7 @@ def set_mobile_device_config(mobile_device_id=None, **kwargs):
if str(mobile_device_id).isdigit(): if str(mobile_device_id).isdigit():
mobile_device_id = int(mobile_device_id) mobile_device_id = int(mobile_device_id)
else: else:
logger.error("Tautulli MobileApp :: Unable to set exisiting mobile device: invalid mobile_device_id %s." % mobile_device_id) logger.error("Tautulli MobileApp :: Unable to set existing mobile device: invalid mobile_device_id %s." % mobile_device_id)
return False return False
keys = {'id': mobile_device_id} keys = {'id': mobile_device_id}

View file

@ -955,7 +955,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
now_iso = now.isocalendar() now_iso = now.isocalendar()
available_params = { available_params = {
# Global paramaters # Global parameters
'tautulli_version': common.RELEASE, 'tautulli_version': common.RELEASE,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE, 'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH, 'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
@ -1082,6 +1082,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'stream_audio_sample_rate': notify_params['stream_audio_sample_rate'], 'stream_audio_sample_rate': notify_params['stream_audio_sample_rate'],
'stream_audio_language': notify_params['stream_audio_language'], 'stream_audio_language': notify_params['stream_audio_language'],
'stream_audio_language_code': notify_params['stream_audio_language_code'], 'stream_audio_language_code': notify_params['stream_audio_language_code'],
'stream_audio_profile': notify_params['stream_audio_profile'],
'stream_subtitle_codec': notify_params['stream_subtitle_codec'], 'stream_subtitle_codec': notify_params['stream_subtitle_codec'],
'stream_subtitle_container': notify_params['stream_subtitle_container'], 'stream_subtitle_container': notify_params['stream_subtitle_container'],
'stream_subtitle_format': notify_params['stream_subtitle_format'], 'stream_subtitle_format': notify_params['stream_subtitle_format'],
@ -1215,6 +1216,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'audio_sample_rate': notify_params['audio_sample_rate'], 'audio_sample_rate': notify_params['audio_sample_rate'],
'audio_language': notify_params['audio_language'], 'audio_language': notify_params['audio_language'],
'audio_language_code': notify_params['audio_language_code'], 'audio_language_code': notify_params['audio_language_code'],
'audio_profile': notify_params['audio_profile'],
'subtitle_codec': notify_params['subtitle_codec'], 'subtitle_codec': notify_params['subtitle_codec'],
'subtitle_container': notify_params['subtitle_container'], 'subtitle_container': notify_params['subtitle_container'],
'subtitle_format': notify_params['subtitle_format'], 'subtitle_format': notify_params['subtitle_format'],
@ -1267,7 +1269,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
now_iso = now.isocalendar() now_iso = now.isocalendar()
available_params = { available_params = {
# Global paramaters # Global parameters
'tautulli_version': common.RELEASE, 'tautulli_version': common.RELEASE,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE, 'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH, 'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,

View file

@ -2667,6 +2667,7 @@ class NTFY(Notifier):
provider_name = pretty_metadata.get_provider_name(provider) provider_name = pretty_metadata.get_provider_name(provider)
provider_link = pretty_metadata.get_provider_link(provider) provider_link = pretty_metadata.get_provider_link(provider)
if provider_link:
actions.append(f"view, View on {provider_name}, {provider_link}, clear=true") actions.append(f"view, View on {provider_name}, {provider_link}, clear=true")
if self.config['incl_pmslink']: if self.config['incl_pmslink']:
@ -3390,8 +3391,11 @@ class PUSHOVER(Notifier):
image = pretty_metadata.get_image() image = pretty_metadata.get_image()
if image: if image:
if len(image[1]) <= 5242880: # 5MB max attachment size
files = {'attachment': image} files = {'attachment': image}
headers = {} headers = {}
else:
logger.warn("Tautulli Notifiers :: Image size exceeds 5MB limit for {name}.".format(name=self.NAME))
return self.make_request('https://api.pushover.net/1/messages.json', headers=headers, data=data, files=files) return self.make_request('https://api.pushover.net/1/messages.json', headers=headers, data=data, files=files)
@ -4055,7 +4059,7 @@ class TAUTULLIREMOTEAPP(Notifier):
} }
else: else:
logger.warn("Tautulli Notifiers :: Cryptography library is missing. " logger.warn("Tautulli Notifiers :: Cryptography library is missing. "
"Tautulli Remote app notifications will be sent unecrypted. " "Tautulli Remote app notifications will be sent unencrypted. "
"Install the library to encrypt the notifications.") "Install the library to encrypt the notifications.")
payload = {'app_id': mobile_app._ONESIGNAL_APP_ID, payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
@ -4460,7 +4464,8 @@ class WEBHOOK(Notifier):
'select_options': {'GET': 'GET', 'select_options': {'GET': 'GET',
'POST': 'POST', 'POST': 'POST',
'PUT': 'PUT', 'PUT': 'PUT',
'DELETE': 'DELETE'} 'DELETE': 'DELETE',
'PATCH': 'PATCH'}
} }
] ]

View file

@ -2096,6 +2096,7 @@ class PmsConnect(object):
'stream_audio_channel_layout_': stream_audio_channel_layouts_, 'stream_audio_channel_layout_': stream_audio_channel_layouts_,
'stream_audio_language': helpers.get_xml_attr(audio_stream_info, 'language'), 'stream_audio_language': helpers.get_xml_attr(audio_stream_info, 'language'),
'stream_audio_language_code': helpers.get_xml_attr(audio_stream_info, 'languageCode'), 'stream_audio_language_code': helpers.get_xml_attr(audio_stream_info, 'languageCode'),
'stream_audio_profile': helpers.get_xml_attr(audio_stream_info, 'profile'),
'stream_audio_decision': helpers.get_xml_attr(audio_stream_info, 'decision') or 'direct play' 'stream_audio_decision': helpers.get_xml_attr(audio_stream_info, 'decision') or 'direct play'
} }
else: else:
@ -2108,6 +2109,7 @@ class PmsConnect(object):
'stream_audio_channel_layout_': '', 'stream_audio_channel_layout_': '',
'stream_audio_language': '', 'stream_audio_language': '',
'stream_audio_language_code': '', 'stream_audio_language_code': '',
'stream_audio_profile': '',
'stream_audio_decision': '' 'stream_audio_decision': ''
} }

View file

@ -50,7 +50,7 @@ def plex_user_login(token=None, headers=None):
user_token = None user_token = None
user_id = None user_id = None
# Try to login to Plex.tv to check if the user has a vaild account # Try to login to Plex.tv to check if the user has a valid account
if token: if token:
plex_tv = PlexTV(token=token, headers=headers) plex_tv = PlexTV(token=token, headers=headers)
plex_user = plex_tv.get_plex_account_details() plex_user = plex_tv.get_plex_account_details()
@ -176,7 +176,10 @@ def check_auth(*args, **kwargs):
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT) raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
else: else:
redirect_uri = cherrypy.request.wsgi_environ['REQUEST_URI'] if cherrypy.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
raise cherrypy.HTTPError(401)
redirect_uri = cherrypy.request.path_info
if redirect_uri: if redirect_uri:
redirect_uri = '?redirect_uri=' + quote(redirect_uri) redirect_uri = '?redirect_uri=' + quote(redirect_uri)

View file

@ -608,7 +608,7 @@ class WebInterface(object):
status_message = '' status_message = ''
else: else:
result = None result = None
status_message = 'An error occured.' status_message = 'An error occurred.'
return serve_template(template_name="edit_library.html", title="Edit Library", return serve_template(template_name="edit_library.html", title="Edit Library",
data=result, server_id=plexpy.CONFIG.PMS_IDENTIFIER, status_message=status_message) data=result, server_id=plexpy.CONFIG.PMS_IDENTIFIER, status_message=status_message)
@ -1347,7 +1347,7 @@ class WebInterface(object):
status_message = '' status_message = ''
else: else:
result = None result = None
status_message = 'An error occured.' status_message = 'An error occurred.'
return serve_template(template_name="edit_user.html", title="Edit User", data=result, status_message=status_message) return serve_template(template_name="edit_user.html", title="Edit User", data=result, status_message=status_message)
@ -1365,7 +1365,7 @@ class WebInterface(object):
keep_history (int): 0 or 1 keep_history (int): 0 or 1
allow_guest (int): 0 or 1 allow_guest (int): 0 or 1
Optional paramters: Optional parameters:
None None
Returns: Returns:
@ -3031,7 +3031,7 @@ class WebInterface(object):
""" Delete the Tautulli notification logs. """ Delete the Tautulli notification logs.
``` ```
Required paramters: Required parameters:
None None
Optional parameters: Optional parameters:
@ -3056,7 +3056,7 @@ class WebInterface(object):
""" Delete the Tautulli newsletter logs. """ Delete the Tautulli newsletter logs.
``` ```
Required paramters: Required parameters:
None None
Optional parameters: Optional parameters:
@ -3081,7 +3081,7 @@ class WebInterface(object):
""" Delete the Tautulli login logs. """ Delete the Tautulli login logs.
``` ```
Required paramters: Required parameters:
None None
Optional parameters: Optional parameters:
@ -5921,6 +5921,7 @@ class WebInterface(object):
"stream_audio_decision": "direct play", "stream_audio_decision": "direct play",
"stream_audio_language": "", "stream_audio_language": "",
"stream_audio_language_code": "", "stream_audio_language_code": "",
"stream_audio_profile": "",
"stream_audio_sample_rate": "48000", "stream_audio_sample_rate": "48000",
"stream_bitrate": "10617", "stream_bitrate": "10617",
"stream_container": "mkv", "stream_container": "mkv",

View file

@ -21,6 +21,7 @@ import sys
import cheroot.errors import cheroot.errors
import cherrypy import cherrypy
import cherrypy_cors
import plexpy import plexpy
from plexpy import logger from plexpy import logger
@ -62,6 +63,7 @@ def restart():
def initialize(options): def initialize(options):
cherrypy_cors.install()
# HTTPS stuff stolen from sickbeard # HTTPS stuff stolen from sickbeard
enable_https = options['enable_https'] enable_https = options['enable_https']
@ -91,7 +93,8 @@ def initialize(options):
'server.socket_timeout': 60, 'server.socket_timeout': 60,
'tools.encode.on': True, 'tools.encode.on': True,
'tools.encode.encoding': 'utf-8', 'tools.encode.encoding': 'utf-8',
'tools.decode.on': True 'tools.decode.on': True,
'cors.expose.on': True,
} }
if plexpy.DEV: if plexpy.DEV:

View file

@ -5,6 +5,7 @@ bleach==6.2.0
certifi==2024.8.30 certifi==2024.8.30
cheroot==10.0.1 cheroot==10.0.1
cherrypy==18.10.0 cherrypy==18.10.0
cherrypy-cors==1.7.0
cloudinary==1.41.0 cloudinary==1.41.0
distro==1.9.0 distro==1.9.0
dnspython==2.7.0 dnspython==2.7.0