mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11:15 -07:00
Compare commits
No commits in common. "master" and "v2.15.0" have entirely different histories.
35 changed files with 90 additions and 503 deletions
18
.github/workflows/publish-installers.yml
vendored
18
.github/workflows/publish-installers.yml
vendored
|
@ -100,24 +100,6 @@ jobs:
|
||||||
name: Tautulli-${{ matrix.os }}-installer
|
name: Tautulli-${{ matrix.os }}-installer
|
||||||
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }}
|
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }}
|
||||||
|
|
||||||
virus-total:
|
|
||||||
name: VirusTotal Scan
|
|
||||||
needs: build-installer
|
|
||||||
if: needs.build-installer.result == 'success' && !contains(github.event.head_commit.message, '[skip ci]')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Download Installers
|
|
||||||
if: needs.build-installer.result == 'success'
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
|
|
||||||
- name: Upload to VirusTotal
|
|
||||||
uses: crazy-max/ghaction-virustotal@v4
|
|
||||||
with:
|
|
||||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
|
||||||
files: |
|
|
||||||
Tautulli-windows-installer/Tautulli-windows-*-x64.exe
|
|
||||||
Tautulli-macos-installer/Tautulli-macos-*-universal.pkg
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release Installers
|
name: Release Installers
|
||||||
needs: build-installer
|
needs: build-installer
|
||||||
|
|
14
.github/workflows/submit-winget.yml
vendored
14
.github/workflows/submit-winget.yml
vendored
|
@ -23,17 +23,3 @@ jobs:
|
||||||
# getting latest wingetcreate file
|
# getting latest wingetcreate file
|
||||||
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
|
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
|
||||||
.\wingetcreate.exe update $wingetPackage -s -v $version -u $installerUrl -t $gitToken
|
.\wingetcreate.exe update $wingetPackage -s -v $version -u $installerUrl -t $gitToken
|
||||||
|
|
||||||
virus-total:
|
|
||||||
name: VirusTotal Scan
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Upload to VirusTotal
|
|
||||||
uses: crazy-max/ghaction-virustotal@v4
|
|
||||||
with:
|
|
||||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
|
||||||
github_token: ${{ secrets.GHACTIONS_TOKEN }}
|
|
||||||
update_release_body: true
|
|
||||||
files: |
|
|
||||||
.exe$
|
|
||||||
.pkg$
|
|
||||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,40 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v2.15.2 (2025-04-12)
|
|
||||||
|
|
||||||
* Activity:
|
|
||||||
* New: Added link to library by clicking media type icon.
|
|
||||||
* New: Added stream count to tab title on homepage. (#2517)
|
|
||||||
* History:
|
|
||||||
* Fix: Check stream watched status before stream stopped status. (#2506)
|
|
||||||
* Notifications:
|
|
||||||
* Fix: ntfy notifications failing to send if provider link is blank.
|
|
||||||
* Fix: Check Pushover notification attachment is under 5MB limit. (#2396)
|
|
||||||
* Fix: Track URLs redirecting to the correct media page. (#2513)
|
|
||||||
* New: Added audio profile notification parameters.
|
|
||||||
* New: Added PATCH method for Webhook notifications.
|
|
||||||
* Graphs:
|
|
||||||
* New: Added Total line to daily streams graph. (Thanks @zdimension) (#2497)
|
|
||||||
* UI:
|
|
||||||
* Fix: Do not redirect API requests to the login page. (#2490)
|
|
||||||
* Change: Swap source and stream columns in stream info modal.
|
|
||||||
* Other:
|
|
||||||
* Fix: Various typos. (Thanks @luzpaz) (#2520)
|
|
||||||
* Fix: CherryPy CORS response header not being set correctly. (#2279)
|
|
||||||
|
|
||||||
|
|
||||||
## v2.15.1 (2025-01-11)
|
|
||||||
|
|
||||||
* Activity:
|
|
||||||
* Fix: Detection of HDR transcodes. (Thanks @cdecker08) (#2412, #2466)
|
|
||||||
* Newsletters:
|
|
||||||
* Fix: Disable basic authentication for /newsletter and /image endpoints. (#2472)
|
|
||||||
* Exporter:
|
|
||||||
* New: Added logos to season and episode exports.
|
|
||||||
* Other:
|
|
||||||
* Fix: Docker container https health check.
|
|
||||||
|
|
||||||
|
|
||||||
## v2.15.0 (2024-11-24)
|
## v2.15.0 (2024-11-24)
|
||||||
|
|
||||||
* Notes:
|
* Notes:
|
||||||
|
|
|
@ -25,4 +25,4 @@ CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
||||||
ENTRYPOINT [ "./start.sh" ]
|
ENTRYPOINT [ "./start.sh" ]
|
||||||
|
|
||||||
EXPOSE 8181
|
EXPOSE 8181
|
||||||
HEALTHCHECK --start-period=90s CMD curl -ILfks https://localhost:8181/status > /dev/null || curl -ILfs http://localhost:8181/status > /dev/null || exit 1
|
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1
|
||||||
|
|
|
@ -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. Commercial users must licence this software, for more information visit
|
non-commercial use. Commerical 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ def main():
|
||||||
if args.quiet:
|
if args.quiet:
|
||||||
plexpy.QUIET = True
|
plexpy.QUIET = True
|
||||||
|
|
||||||
# Do an initial setup of the logger.
|
# Do an intial 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)
|
||||||
|
|
||||||
|
|
|
@ -4325,10 +4325,6 @@ 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;
|
||||||
|
|
|
@ -74,7 +74,6 @@ 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']) if data['section_id'] else '#'
|
|
||||||
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']}"
|
||||||
|
@ -464,27 +463,21 @@ 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">
|
||||||
<a href="${library_href}">
|
<i class="fa fa-fw fa-broadcast-tower"></i>
|
||||||
<i class="fa fa-fw fa-broadcast-tower"></i>
|
|
||||||
</a>
|
|
||||||
</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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<i class="fa fa-fw fa-video-camera"></i>
|
% endif
|
||||||
% else:
|
|
||||||
<i class="fa fa-fw fa-question-circle"></i>
|
|
||||||
% endif
|
|
||||||
</a>
|
|
||||||
</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">
|
||||||
|
|
|
@ -301,10 +301,6 @@
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
if (!("Total" in chart_visibility)) {
|
|
||||||
chart_visibility["Total"] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data_series.map(function(s) {
|
return data_series.map(function(s) {
|
||||||
var obj = Object.assign({}, s);
|
var obj = Object.assign({}, s);
|
||||||
obj.visible = (chart_visibility[s.name] !== false);
|
obj.visible = (chart_visibility[s.name] !== false);
|
||||||
|
@ -331,8 +327,7 @@
|
||||||
'Direct Play': '#E5A00D',
|
'Direct Play': '#E5A00D',
|
||||||
'Direct Stream': '#FFFFFF',
|
'Direct Stream': '#FFFFFF',
|
||||||
'Transcode': '#F06464',
|
'Transcode': '#F06464',
|
||||||
'Max. Concurrent Streams': '#96C83C',
|
'Max. Concurrent Streams': '#96C83C'
|
||||||
'Total': '#96C83C'
|
|
||||||
};
|
};
|
||||||
var series_colors = [];
|
var series_colors = [];
|
||||||
$.each(data_series, function(index, series) {
|
$.each(data_series, function(index, series) {
|
||||||
|
|
|
@ -298,8 +298,6 @@
|
||||||
|
|
||||||
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||||
|
|
||||||
var title = document.title;
|
|
||||||
|
|
||||||
function getCurrentActivity() {
|
function getCurrentActivity() {
|
||||||
activity_ready = false;
|
activity_ready = false;
|
||||||
|
|
||||||
|
@ -370,8 +368,6 @@
|
||||||
|
|
||||||
$('#currentActivityHeader').show();
|
$('#currentActivityHeader').show();
|
||||||
|
|
||||||
document.title = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' | ' + title;
|
|
||||||
|
|
||||||
sessions.forEach(function (session) {
|
sessions.forEach(function (session) {
|
||||||
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
|
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
|
||||||
var key = s.session_key;
|
var key = s.session_key;
|
||||||
|
@ -604,8 +600,6 @@
|
||||||
} else {
|
} else {
|
||||||
$('#currentActivityHeader').hide();
|
$('#currentActivityHeader').hide();
|
||||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>');
|
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>');
|
||||||
|
|
||||||
document.title = title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activity_ready = true;
|
activity_ready = true;
|
||||||
|
|
|
@ -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 class="heading">
|
|
||||||
Source Details
|
|
||||||
</th>
|
</th>
|
||||||
<th><i class="fa fa-long-arrow-right"></i></th>
|
|
||||||
<th class="heading">
|
<th class="heading">
|
||||||
Stream Details
|
Stream Details
|
||||||
</th>
|
</th>
|
||||||
|
<th class="heading">
|
||||||
|
Source Details
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
|
@ -85,46 +85,38 @@ 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['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>
|
<td>${data['stream_bitrate']} ${'kbps' if data['stream_bitrate'] else ''}</td>
|
||||||
|
<td>${data['bitrate']} ${'kbps' if data['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['video_full_resolution']}</td>
|
|
||||||
<td><i class="fa fa-long-arrow-right"></i></td>
|
|
||||||
<td>${data['stream_video_full_resolution']}</td>
|
<td>${data['stream_video_full_resolution']}</td>
|
||||||
|
<td>${data['video_full_resolution']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
<td>Quality</td>
|
<td>Quality</td>
|
||||||
<td>-</td>
|
|
||||||
<td></td>
|
|
||||||
<td>${data['quality_profile']}</td>
|
<td>${data['quality_profile']}</td>
|
||||||
|
<td>-</td>
|
||||||
</tr>
|
</tr>
|
||||||
% if data['optimized_version'] == 1:
|
% if data['optimized_version'] == 1:
|
||||||
<tr>
|
<tr>
|
||||||
<td>Optimized Version</td>
|
<td>Optimized Version</td>
|
||||||
<td>${data['optimized_version_profile']}<br>(${data['optimized_version_title']})</td>
|
|
||||||
<td></td>
|
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td>${data['optimized_version_profile']}<br>(${data['optimized_version_title']})</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>${data['synced_version_profile']}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td>${data['synced_version_profile']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -135,8 +127,6 @@ DOCUMENTATION :: END
|
||||||
<th>
|
<th>
|
||||||
Container
|
Container
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th>
|
<th>
|
||||||
${data['stream_container_decision']}
|
${data['stream_container_decision']}
|
||||||
</th>
|
</th>
|
||||||
|
@ -145,9 +135,8 @@ DOCUMENTATION :: END
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Container</td>
|
<td>Container</td>
|
||||||
<td>${data['container'].upper()}</td>
|
|
||||||
<td><i class="fa fa-long-arrow-right"></i></td>
|
|
||||||
<td>${data['stream_container'].upper()}</td>
|
<td>${data['stream_container'].upper()}</td>
|
||||||
|
<td>${data['container'].upper()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -158,8 +147,6 @@ DOCUMENTATION :: END
|
||||||
<th>
|
<th>
|
||||||
Video
|
Video
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th>
|
<th>
|
||||||
${data['stream_video_decision']}
|
${data['stream_video_decision']}
|
||||||
</th>
|
</th>
|
||||||
|
@ -168,45 +155,38 @@ DOCUMENTATION :: END
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Codec</td>
|
<td>Codec</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>
|
<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>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Bitrate</td>
|
<td>Bitrate</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>
|
<td>${data['stream_video_bitrate']} ${'kbps' if data['stream_video_bitrate'] else ''}</td>
|
||||||
|
<td>${data['video_bitrate']} ${'kbps' if data['video_bitrate'] else ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Width</td>
|
<td>Width</td>
|
||||||
<td>${data['video_width']}</td>
|
|
||||||
<td><i class="fa fa-long-arrow-right"></i></td>
|
|
||||||
<td>${data['stream_video_width']}</td>
|
<td>${data['stream_video_width']}</td>
|
||||||
|
<td>${data['video_width']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Height</td>
|
<td>Height</td>
|
||||||
<td>${data['video_height']}</td>
|
|
||||||
<td><i class="fa fa-long-arrow-right"></i></td>
|
|
||||||
<td>${data['stream_video_height']}</td>
|
<td>${data['stream_video_height']}</td>
|
||||||
|
<td>${data['video_height']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Framerate</td>
|
<td>Framerate</td>
|
||||||
<td>${data['video_framerate']}</td>
|
|
||||||
<td><i class="fa fa-long-arrow-right"></i></td>
|
|
||||||
<td>${data['stream_video_framerate']}</td>
|
<td>${data['stream_video_framerate']}</td>
|
||||||
|
<td>${data['video_framerate']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Dynamic Range</td>
|
<td>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>
|
<td>${data['stream_video_dynamic_range']}</td>
|
||||||
|
<td>${data['video_dynamic_range']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Aspect Ratio</td>
|
<td>Aspect Ratio</td>
|
||||||
<td>${data['aspect_ratio']}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td>${data['aspect_ratio']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -217,8 +197,6 @@ DOCUMENTATION :: END
|
||||||
<th>
|
<th>
|
||||||
Audio
|
Audio
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th>
|
<th>
|
||||||
${data['stream_audio_decision']}
|
${data['stream_audio_decision']}
|
||||||
</th>
|
</th>
|
||||||
|
@ -227,27 +205,23 @@ DOCUMENTATION :: END
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Codec</td>
|
<td>Codec</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>
|
<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>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Bitrate</td>
|
<td>Bitrate</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>
|
<td>${data['stream_audio_bitrate']} ${'kbps' if data['stream_audio_bitrate'] else ''}</td>
|
||||||
|
<td>${data['audio_bitrate']} ${'kbps' if data['audio_bitrate'] else ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Channels</td>
|
<td>Channels</td>
|
||||||
<td>${data['audio_channels']}</td>
|
|
||||||
<td><i class="fa fa-long-arrow-right"></i></td>
|
|
||||||
<td>${data['stream_audio_channels']}</td>
|
<td>${data['stream_audio_channels']}</td>
|
||||||
|
<td>${data['audio_channels']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Language</td>
|
<td>Language</td>
|
||||||
<td>${data['audio_language'] or 'Unknown'}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td>${data['audio_language'] or 'Unknown'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -259,8 +233,6 @@ 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>
|
||||||
|
@ -269,22 +241,19 @@ DOCUMENTATION :: END
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Codec</td>
|
<td>Codec</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>
|
<td>${data['stream_subtitle_codec'].upper() or '-'}</td>
|
||||||
|
<td>${data['subtitle_codec'].upper()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Language</td>
|
<td>Language</td>
|
||||||
<td>${data['subtitle_language'] or 'Unknown'}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td>${data['subtitle_language'] or 'Unknown'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% if data['subtitle_forced']:
|
% if data['subtitle_forced']:
|
||||||
<tr>
|
<tr>
|
||||||
<td>Forced</td>
|
<td>Forced</td>
|
||||||
<td>${bool(data['subtitle_forced'])}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td>${bool(data['subtitle_forced'])}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -1,255 +0,0 @@
|
||||||
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)
|
|
|
@ -27,7 +27,7 @@ from .exceptions import (
|
||||||
)
|
)
|
||||||
from .jwks_client import PyJWKClient
|
from .jwks_client import PyJWKClient
|
||||||
|
|
||||||
__version__ = "2.10.1"
|
__version__ = "2.10.0"
|
||||||
|
|
||||||
__title__ = "PyJWT"
|
__title__ = "PyJWT"
|
||||||
__description__ = "JSON Web Token implementation in Python"
|
__description__ = "JSON Web Token implementation in Python"
|
||||||
|
|
|
@ -419,11 +419,11 @@ class PyJWT:
|
||||||
if "iss" not in payload:
|
if "iss" not in payload:
|
||||||
raise MissingRequiredClaimError("iss")
|
raise MissingRequiredClaimError("iss")
|
||||||
|
|
||||||
if isinstance(issuer, str):
|
if isinstance(issuer, Sequence):
|
||||||
if payload["iss"] != issuer:
|
if payload["iss"] not in issuer:
|
||||||
raise InvalidIssuerError("Invalid issuer")
|
raise InvalidIssuerError("Invalid issuer")
|
||||||
else:
|
else:
|
||||||
if payload["iss"] not in issuer:
|
if payload["iss"] != issuer:
|
||||||
raise InvalidIssuerError("Invalid issuer")
|
raise InvalidIssuerError("Invalid issuer")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
# Library version
|
# Library version
|
||||||
MAJOR_VERSION = 4
|
MAJOR_VERSION = 4
|
||||||
MINOR_VERSION = 16
|
MINOR_VERSION = 16
|
||||||
PATCH_VERSION = 1
|
PATCH_VERSION = 0
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from plexapi import CONFIG, X_PLEX_IDENTIFIER, TIMEOUT
|
from plexapi import CONFIG, X_PLEX_IDENTIFIER
|
||||||
from plexapi.client import PlexClient
|
from plexapi.client import PlexClient
|
||||||
from plexapi.exceptions import BadRequest
|
from plexapi.exceptions import BadRequest
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
|
@ -46,7 +46,7 @@ class PlexSonosClient(PlexClient):
|
||||||
_session (obj): Requests session object used to access this client.
|
_session (obj): Requests session object used to access this client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, account, data, timeout=None):
|
def __init__(self, account, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
self.deviceClass = data.attrib.get("deviceClass")
|
self.deviceClass = data.attrib.get("deviceClass")
|
||||||
self.machineIdentifier = data.attrib.get("machineIdentifier")
|
self.machineIdentifier = data.attrib.get("machineIdentifier")
|
||||||
|
@ -66,7 +66,6 @@ class PlexSonosClient(PlexClient):
|
||||||
self._last_call = 0
|
self._last_call = 0
|
||||||
self._proxyThroughServer = False
|
self._proxyThroughServer = False
|
||||||
self._showSecrets = CONFIG.get("log.show_secrets", "").lower() == "true"
|
self._showSecrets = CONFIG.get("log.show_secrets", "").lower() == "true"
|
||||||
self._timeout = timeout or TIMEOUT
|
|
||||||
|
|
||||||
def playMedia(self, media, offset=0, **params):
|
def playMedia(self, media, offset=0, **params):
|
||||||
|
|
||||||
|
|
|
@ -716,7 +716,7 @@ class Show(
|
||||||
class Season(
|
class Season(
|
||||||
Video,
|
Video,
|
||||||
AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
|
AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
|
||||||
ArtMixin, LogoMixin, PosterMixin, ThemeUrlMixin,
|
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||||
SeasonEditMixins
|
SeasonEditMixins
|
||||||
):
|
):
|
||||||
""" Represents a single Season.
|
""" Represents a single Season.
|
||||||
|
@ -883,7 +883,7 @@ class Season(
|
||||||
class Episode(
|
class Episode(
|
||||||
Video, Playable,
|
Video, Playable,
|
||||||
ExtrasMixin, RatingMixin,
|
ExtrasMixin, RatingMixin,
|
||||||
ArtMixin, LogoMixin, PosterMixin, ThemeUrlMixin,
|
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||||
EpisodeEditMixins
|
EpisodeEditMixins
|
||||||
):
|
):
|
||||||
""" Represents a single Episode.
|
""" Represents a single Episode.
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
!define APP_NAME "Tautulli"
|
!define APP_NAME "Tautulli"
|
||||||
!define COMP_NAME "Tautulli"
|
!define COMP_NAME "Tautulli"
|
||||||
!define WEB_SITE "https://tautulli.com"
|
!define WEB_SITE "https://tautulli.com"
|
||||||
!define COPYRIGHT "Tautulli © 2025"
|
!define COPYRIGHT "Tautulli © 2020"
|
||||||
!define DESCRIPTION "Monitor your Plex Media Server"
|
!define DESCRIPTION "Monitor your Plex Media Server"
|
||||||
!define APP_ICON "..\dist\Tautulli\data\interfaces\default\images\logo-circle.ico"
|
!define APP_ICON "..\dist\Tautulli\data\interfaces\default\images\logo-circle.ico"
|
||||||
!define LICENSE_TXT "..\dist\Tautulli\LICENSE"
|
!define LICENSE_TXT "..\dist\Tautulli\LICENSE"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
apscheduler==3.10.1
|
apscheduler==3.10.1
|
||||||
cryptography==44.0.2
|
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.10.0
|
pyinstaller==6.11.1
|
||||||
pyopenssl==25.0.0
|
pyopenssl==24.2.1
|
||||||
|
|
||||||
pyobjc-core==10.3.1; platform_system == "Darwin"
|
pyobjc-core==10.3.1; platform_system == "Darwin"
|
||||||
pyobjc-framework-Cocoa==10.3.1; platform_system == "Darwin"
|
pyobjc-framework-Cocoa==10.3.1; platform_system == "Darwin"
|
||||||
|
|
|
@ -314,10 +314,6 @@ 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
|
||||||
|
@ -358,6 +354,10 @@ 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()
|
||||||
|
|
|
@ -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 database successfully, remove the session from the session table
|
# If session is written to the databaase 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)
|
||||||
|
|
|
@ -476,7 +476,6 @@ 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.'},
|
||||||
|
@ -607,7 +606,6 @@ 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.'},
|
||||||
|
|
|
@ -568,7 +568,7 @@ class Config(object):
|
||||||
|
|
||||||
def _upgrade(self):
|
def _upgrade(self):
|
||||||
"""
|
"""
|
||||||
Upgrades config file from previous versions and bumps up config version
|
Upgrades config file from previous verisions and bumps up config version
|
||||||
"""
|
"""
|
||||||
if self.CONFIG_VERSION == 0:
|
if self.CONFIG_VERSION == 0:
|
||||||
self.CONFIG_VERSION = 1
|
self.CONFIG_VERSION = 1
|
||||||
|
|
|
@ -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 separated list of the exact column variables received.
|
# column_string is a comma seperated 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.
|
||||||
|
|
|
@ -40,8 +40,8 @@ class Export(object):
|
||||||
MEDIA_TYPES = {
|
MEDIA_TYPES = {
|
||||||
'movie': (True, True, True),
|
'movie': (True, True, True),
|
||||||
'show': (True, True, True),
|
'show': (True, True, True),
|
||||||
'season': (True, True, True),
|
'season': (True, True, False),
|
||||||
'episode': (False, False, True),
|
'episode': (False, False, False),
|
||||||
'artist': (True, True, False),
|
'artist': (True, True, False),
|
||||||
'album': (True, True, False),
|
'album': (True, True, False),
|
||||||
'track': (False, False, False),
|
'track': (False, False, False),
|
||||||
|
@ -533,9 +533,6 @@ class Export(object):
|
||||||
'librarySectionID': None,
|
'librarySectionID': None,
|
||||||
'librarySectionKey': None,
|
'librarySectionKey': None,
|
||||||
'librarySectionTitle': None,
|
'librarySectionTitle': None,
|
||||||
'logo': lambda o: next((i.url for i in o.images if i.type == 'clearLogo'), None),
|
|
||||||
'logoFile': lambda o: self.get_image(o, 'logo'),
|
|
||||||
'logoProvider': lambda o: self.get_image_provider(o, 'logo'),
|
|
||||||
'metadataDirectory': None,
|
'metadataDirectory': None,
|
||||||
'parentGuid': None,
|
'parentGuid': None,
|
||||||
'parentIndex': None,
|
'parentIndex': None,
|
||||||
|
@ -629,9 +626,6 @@ class Export(object):
|
||||||
'librarySectionKey': None,
|
'librarySectionKey': None,
|
||||||
'librarySectionTitle': None,
|
'librarySectionTitle': None,
|
||||||
'locations': None,
|
'locations': None,
|
||||||
'logo': lambda o: next((i.url for i in o.images if i.type == 'clearLogo'), None),
|
|
||||||
'logoFile': lambda o: self.get_image(o, 'logo'),
|
|
||||||
'logoProvider': lambda o: self.get_image_provider(o, 'logo'),
|
|
||||||
'markers': {
|
'markers': {
|
||||||
'end': None,
|
'end': None,
|
||||||
'final': None,
|
'final': None,
|
||||||
|
|
|
@ -138,12 +138,6 @@ class Graphs(object):
|
||||||
if libraries.has_library_type('live'):
|
if libraries.has_library_type('live'):
|
||||||
series_output.append(series_4_output)
|
series_output.append(series_4_output)
|
||||||
|
|
||||||
if len(series_output) > 0:
|
|
||||||
series_total = [sum(x) for x in zip(*[x['data'] for x in series_output])]
|
|
||||||
series_total_output = {'name': 'Total',
|
|
||||||
'data': series_total}
|
|
||||||
series_output.append(series_total_output)
|
|
||||||
|
|
||||||
output = {'categories': categories,
|
output = {'categories': categories,
|
||||||
'series': series_output}
|
'series': series_output}
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -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 existing mobile device: invalid mobile_device_id %s." % mobile_device_id)
|
logger.error("Tautulli MobileApp :: Unable to set exisiting mobile device: invalid mobile_device_id %s." % mobile_device_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
keys = {'id': mobile_device_id}
|
keys = {'id': mobile_device_id}
|
||||||
|
|
|
@ -684,23 +684,23 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||||
thetvdb_media_type = 'movie' if notify_params['media_type'] == 'movie' else 'series'
|
thetvdb_media_type = 'movie' if notify_params['media_type'] == 'movie' else 'series'
|
||||||
notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
|
notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
|
||||||
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/{thetvdb_media_type}/{notify_params["thetvdb_id"]}'
|
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/{thetvdb_media_type}/{notify_params["thetvdb_id"]}'
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show'
|
||||||
|
|
||||||
elif 'thetvdbdvdorder://' in notify_params['guid']:
|
elif 'thetvdbdvdorder://' in notify_params['guid']:
|
||||||
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0].split('?')[0]
|
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0].split('?')[0]
|
||||||
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/series/{notify_params["thetvdb_id"]}'
|
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/series/{notify_params["thetvdb_id"]}'
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show'
|
||||||
|
|
||||||
if 'themoviedb://' in notify_params['guid'] or notify_params['themoviedb_id']:
|
if 'themoviedb://' in notify_params['guid'] or notify_params['themoviedb_id']:
|
||||||
if notify_params['media_type'] == 'movie':
|
if notify_params['media_type'] == 'movie':
|
||||||
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('?')[0]
|
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('?')[0]
|
||||||
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id']
|
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id']
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?type=movie'
|
||||||
|
|
||||||
elif notify_params['media_type'] in ('show', 'season', 'episode'):
|
elif notify_params['media_type'] in ('show', 'season', 'episode'):
|
||||||
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0]
|
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0]
|
||||||
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
|
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?type=show'
|
||||||
|
|
||||||
if 'lastfm://' in notify_params['guid']:
|
if 'lastfm://' in notify_params['guid']:
|
||||||
notify_params['lastfm_id'] = '/'.join(notify_params['guid'].split('lastfm://')[1].split('?')[0].split('/')[:2])
|
notify_params['lastfm_id'] = '/'.join(notify_params['guid'].split('lastfm://')[1].split('?')[0].split('/')[:2])
|
||||||
|
@ -765,7 +765,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||||
if themoviedb_info.get('imdb_id'):
|
if themoviedb_info.get('imdb_id'):
|
||||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoviedb_info['imdb_id']
|
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoviedb_info['imdb_id']
|
||||||
if themoviedb_info.get('themoviedb_id'):
|
if themoviedb_info.get('themoviedb_id'):
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/{}?id_type={}'.format(
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/{}?type={}'.format(
|
||||||
notify_params['themoviedb_id'], 'show' if lookup_media_type == 'tv' else 'movie')
|
notify_params['themoviedb_id'], 'show' if lookup_media_type == 'tv' else 'movie')
|
||||||
|
|
||||||
# Get TVmaze info (for tv shows only)
|
# Get TVmaze info (for tv shows only)
|
||||||
|
@ -790,7 +790,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||||
|
|
||||||
if tvmaze_info.get('thetvdb_id'):
|
if tvmaze_info.get('thetvdb_id'):
|
||||||
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/series/{tvmaze_info["thetvdb_id"]}'
|
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/series/{tvmaze_info["thetvdb_id"]}'
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(notify_params['thetvdb_id']) + '?id_type=show'
|
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(notify_params['thetvdb_id']) + '?type=show'
|
||||||
if tvmaze_info.get('imdb_id'):
|
if tvmaze_info.get('imdb_id'):
|
||||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
|
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
|
||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
||||||
|
@ -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 parameters
|
# Global paramaters
|
||||||
'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,7 +1082,6 @@ 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'],
|
||||||
|
@ -1216,7 +1215,6 @@ 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'],
|
||||||
|
@ -1269,7 +1267,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
||||||
now_iso = now.isocalendar()
|
now_iso = now.isocalendar()
|
||||||
|
|
||||||
available_params = {
|
available_params = {
|
||||||
# Global parameters
|
# Global paramaters
|
||||||
'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,
|
||||||
|
|
|
@ -2667,8 +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']:
|
||||||
plex_url = pretty_metadata.get_plex_url()
|
plex_url = pretty_metadata.get_plex_url()
|
||||||
|
@ -3391,11 +3390,8 @@ 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)
|
||||||
|
|
||||||
|
@ -4059,7 +4055,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 unencrypted. "
|
"Tautulli Remote app notifications will be sent unecrypted. "
|
||||||
"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,
|
||||||
|
@ -4464,8 +4460,7 @@ 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'}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -2096,7 +2096,6 @@ 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:
|
||||||
|
@ -2109,7 +2108,6 @@ 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': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3388,10 +3386,10 @@ class PmsConnect(object):
|
||||||
def get_dynamic_range(stream):
|
def get_dynamic_range(stream):
|
||||||
extended_display_title = helpers.get_xml_attr(stream, 'extendedDisplayTitle')
|
extended_display_title = helpers.get_xml_attr(stream, 'extendedDisplayTitle')
|
||||||
bit_depth = helpers.cast_to_int(helpers.get_xml_attr(stream, 'bitDepth'))
|
bit_depth = helpers.cast_to_int(helpers.get_xml_attr(stream, 'bitDepth'))
|
||||||
color_trc = helpers.get_xml_attr(stream, 'colorTrc')
|
color_space = helpers.get_xml_attr(stream, 'colorSpace')
|
||||||
DOVI_profile = helpers.get_xml_attr(stream, 'DOVIProfile')
|
DOVI_profile = helpers.get_xml_attr(stream, 'DOVIProfile')
|
||||||
|
|
||||||
HDR = bool(bit_depth > 8 and (color_trc == 'smpte2084' or color_trc == 'arib-std-b67'))
|
HDR = bool(bit_depth > 8 and 'bt2020' in color_space)
|
||||||
DV = bool(DOVI_profile)
|
DV = bool(DOVI_profile)
|
||||||
|
|
||||||
if not HDR and not DV:
|
if not HDR and not DV:
|
||||||
|
|
|
@ -16,4 +16,4 @@
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
PLEXPY_BRANCH = "master"
|
PLEXPY_BRANCH = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.15.2"
|
PLEXPY_RELEASE_VERSION = "v2.15.0"
|
|
@ -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 valid account
|
# Try to login to Plex.tv to check if the user has a vaild 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,10 +176,7 @@ def check_auth(*args, **kwargs):
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if cherrypy.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
redirect_uri = cherrypy.request.wsgi_environ['REQUEST_URI']
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -608,7 +608,7 @@ class WebInterface(object):
|
||||||
status_message = ''
|
status_message = ''
|
||||||
else:
|
else:
|
||||||
result = None
|
result = None
|
||||||
status_message = 'An error occurred.'
|
status_message = 'An error occured.'
|
||||||
|
|
||||||
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 occurred.'
|
status_message = 'An error occured.'
|
||||||
|
|
||||||
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 parameters:
|
Optional paramters:
|
||||||
None
|
None
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -3031,7 +3031,7 @@ class WebInterface(object):
|
||||||
""" Delete the Tautulli notification logs.
|
""" Delete the Tautulli notification logs.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required paramters:
|
||||||
None
|
None
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
|
@ -3056,7 +3056,7 @@ class WebInterface(object):
|
||||||
""" Delete the Tautulli newsletter logs.
|
""" Delete the Tautulli newsletter logs.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required paramters:
|
||||||
None
|
None
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
|
@ -3081,7 +3081,7 @@ class WebInterface(object):
|
||||||
""" Delete the Tautulli login logs.
|
""" Delete the Tautulli login logs.
|
||||||
|
|
||||||
```
|
```
|
||||||
Required parameters:
|
Required paramters:
|
||||||
None
|
None
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
|
@ -5921,7 +5921,6 @@ 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",
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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
|
||||||
|
@ -63,7 +62,6 @@ 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']
|
||||||
|
@ -93,8 +91,7 @@ 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:
|
||||||
|
@ -174,12 +171,6 @@ def initialize(options):
|
||||||
'/status': {
|
'/status': {
|
||||||
'tools.auth_basic.on': False
|
'tools.auth_basic.on': False
|
||||||
},
|
},
|
||||||
'/newsletter': {
|
|
||||||
'tools.auth_basic.on': False
|
|
||||||
},
|
|
||||||
'/image': {
|
|
||||||
'tools.auth_basic.on': False
|
|
||||||
},
|
|
||||||
'/interfaces': {
|
'/interfaces': {
|
||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
'tools.staticdir.dir': "interfaces",
|
'tools.staticdir.dir': "interfaces",
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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
|
||||||
|
@ -26,10 +25,10 @@ musicbrainzngs==0.7.1
|
||||||
packaging==24.2
|
packaging==24.2
|
||||||
paho-mqtt==2.1.0
|
paho-mqtt==2.1.0
|
||||||
platformdirs==4.3.6
|
platformdirs==4.3.6
|
||||||
plexapi==4.16.1
|
plexapi==4.16.0
|
||||||
portend==3.2.0
|
portend==3.2.0
|
||||||
profilehooks==1.13.0
|
profilehooks==1.13.0
|
||||||
PyJWT==2.10.1
|
PyJWT==2.10.0
|
||||||
pyparsing==3.2.0
|
pyparsing==3.2.0
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
python-twitter==3.5
|
python-twitter==3.5
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue