Compare commits

..

55 Commits

Author SHA1 Message Date
JonnyWong16
27d2c7b078 v2.1.36-beta 2019-10-05 21:59:14 -07:00
JonnyWong16
2fb12ccf65 Fix updating activity card when rating key changes for the same session key (Fixes Tautulli/Tautulli-Issues#96) 2019-10-05 21:48:11 -07:00
JonnyWong16
cb92d159c1 Add parent_guid and grandparent_guid to get_activity and get_metadata API commands 2019-10-05 20:21:15 -07:00
JonnyWong16
64bdf4237c Separate progressive vs. interlaced video on stream type graphs 2019-09-27 13:44:08 -07:00
JonnyWong16
fd7b4ec7e3 Get srouce video_full_resolution from Plex metadata 2019-09-27 13:26:55 -07:00
JonnyWong16
57eb57d4d7 Use video_full_resolution in stream data modal 2019-09-27 13:04:40 -07:00
JonnyWong16
7974e9505b Save session video_scan_type and video_full_resolution to the database 2019-09-27 13:04:17 -07:00
JonnyWong16
7498fb37b5 Update database with video_scan_type and video_full_resolution 2019-09-27 13:03:34 -07:00
JonnyWong16
2cc3e88e6c Fix Albums typo on media info table 2019-09-26 20:14:48 -07:00
JonnyWong16
5fd8cfeb80 v2.1.35-beta 2019-09-24 18:09:05 -07:00
JonnyWong16
b295566a4e Clear metadata cache before retrieving new sessions 2019-09-19 21:01:37 -07:00
JonnyWong16
e0943a2d55 Clear metadata cache on startup check for active sessions 2019-09-19 20:55:19 -07:00
JonnyWong16
3015740c3e Merge pull request #1354 from samwiseg0/fix/blank_audio
Fix blank audio on activity card when changing audio streams during direct play
2019-09-19 19:40:14 -07:00
JonnyWong16
ec9ff2f803 Merge pull request #1353 from samwiseg0/fix/video_scan_type
Add video scan type and standardize video resolution. Fixes Tautulli/Tautulli-Issues#194
2019-09-19 19:39:40 -07:00
JonnyWong16
ec8aae9122 Fix Tautulli logging out after saving settings and restarting 2019-09-19 19:37:31 -07:00
samwiseg0
52e608cc43 Fix blank audio on activity card when changing audio streams 2019-09-19 22:29:44 -04:00
samwiseg0
8213f270e5 Move full resolution outside if statement 2019-09-19 22:26:43 -04:00
JonnyWong16
7085042b0d Merge pull request #1352 from samwiseg0/fix/watch_stats
Add year to SQL query for movie watch stats
2019-09-19 19:04:06 -07:00
JonnyWong16
6a411d2458 Merge pull request #1351 from samwiseg0/fix/release_date
Fix release date to be a string vs integer
2019-09-19 19:03:57 -07:00
JonnyWong16
38e2fbabb8 Merge pull request #1350 from samwiseg0/fix/lin_hw_decode
Fix hardware decode not reflected in GUI
2019-09-19 19:03:48 -07:00
samwiseg0
85709f754a Update API docs to reflect changes 2019-09-19 21:22:37 -04:00
samwiseg0
623a1e8a91 Update webui to utilize video full resolution 2019-09-19 21:22:02 -04:00
samwiseg0
de69945ebe Define notification parameters 2019-09-19 20:57:08 -04:00
samwiseg0
7095fa6ac6 Remove overrides 2019-09-19 20:53:18 -04:00
samwiseg0
a59e8298fd Use video_full_resolution for optimized versions in the activity card 2019-09-19 20:50:17 -04:00
samwiseg0
2737d52279 Set the full resolution of the source video and stream video 2019-09-19 20:47:39 -04:00
samwiseg0
0ac1ad4386 Create video_scan_type and stream_video_scan_type 2019-09-19 20:46:38 -04:00
samwiseg0
2db328ac31 Standardize videoResolution to be lowercase 2019-09-19 20:45:53 -04:00
samwiseg0
b6de4ad054 Add year to SQL query watch stats. Fixes Tautulli/Tautulli-Issues#195 2019-09-16 23:44:48 -04:00
samwiseg0
cfea7164b7 Fix release date to be a str vs int 2019-09-16 22:50:31 -04:00
samwiseg0
7e7e5a6be4 Add nvdec for linux decode support. Fixes Tautulli/Tautulli-Issues#193 2019-09-16 21:30:09 -04:00
samwiseg0
df57f4c009 Remove duplicate nvenc 2019-09-16 21:24:57 -04:00
JonnyWong16
c2185c4ce5 Fix notificaiton parameter prefix and suffix not being substituted correctly 2019-09-07 16:37:38 -07:00
JonnyWong16
08714436c3 v2.1.34 2019-09-03 21:46:48 -07:00
JonnyWong16
f65f5d07c0 Add product to get_history API command 2019-09-03 19:52:54 -07:00
JonnyWong16
a9b10c4560 Add Product column to history tables 2019-09-03 19:52:33 -07:00
JonnyWong16
589fbd3158 Add TVMaze and TMDB IDs to notification parameters after lookup 2019-08-27 19:54:54 -07:00
JonnyWong16
0ffc8c5d19 Update JWT secret instead of UUID 2019-08-24 22:00:31 -07:00
JonnyWong16
7498617b74 Flag update JWT UUID after restarting 2019-08-24 21:13:39 -07:00
JonnyWong16
f21d505ab8 Force logout all clients when changing the admin password 2019-08-24 20:49:56 -07:00
JonnyWong16
7b16af0585 Fix verifying PMS with unpublished hostnames (Fixes Tautulli/Tautulli-Issues#190) 2019-08-16 21:27:34 -07:00
JonnyWong16
a83108282a Fix add title to searchable media info fields 2019-08-14 22:50:23 -07:00
JonnyWong16
1c4d01d6ec Fix libraries/users table respect grouping setting 2019-08-09 19:15:53 -07:00
JonnyWong16
22e6d4067d Missing space for episode titles on tables 2019-08-09 19:01:27 -07:00
JonnyWong16
1046b29c1a v2.1.33 2019-07-27 08:44:26 -07:00
JonnyWong16
d6127e28f3 Remove email sort 2019-07-22 09:03:20 -07:00
JonnyWong16
25a949356d Allow seraching by email address in dropdown menu 2019-07-21 09:46:17 -07:00
JonnyWong16
72a012b817 Update Plex OAuth headers 2019-07-18 11:39:16 -07:00
JonnyWong16
f439bd639c Make sure config has name and value when masking passwords 2019-07-10 21:37:32 -07:00
JonnyWong16
91476a420a Mask notifier and newsletter config passwords (Fixes Tautulli/Tautulli-Issues#172) 2019-07-08 23:50:48 -07:00
JonnyWong16
96c0f9cad5 Add if Docker container to platform 2019-07-08 22:30:58 -07:00
JonnyWong16
df50559495 Fix overwriting version number with None 2019-07-02 08:58:56 -07:00
JonnyWong16
6d35bd7947 v2.1.32 2019-06-26 19:14:30 -07:00
JonnyWong16
d27356bbba Fix timezone error with newsletter scheduler because QNAP devices use a stupid "local" timezone (Fixes Tautulli/Tautulli-Issues#183) 2019-06-26 19:11:33 -07:00
JonnyWong16
3054a824ce v2.1.31 2019-06-24 21:45:57 -07:00
37 changed files with 514 additions and 177 deletions

17
API.md
View File

@@ -416,6 +416,7 @@ Returns:
"Drama", "Drama",
"Fantasy" "Fantasy"
], ],
"grandparent_guid": "com.plexapp.agents.thetvdb://121361?lang=en",
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1503306930", "grandparent_thumb": "/library/metadata/1219/thumb/1503306930",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
@@ -443,6 +444,7 @@ Returns:
"optimized_version_title": "", "optimized_version_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"original_title": "", "original_title": "",
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210", "parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -507,7 +509,9 @@ Returns:
"stream_video_language": "", "stream_video_language": "",
"stream_video_language_code": "", "stream_video_language_code": "",
"stream_video_ref_frames": "4", "stream_video_ref_frames": "4",
"stream_video_full_resolution": "1080p",
"stream_video_resolution": "1080", "stream_video_resolution": "1080",
"stream_video_scan_type": "progressive",
"stream_video_width": "1920", "stream_video_width": "1920",
"studio": "HBO", "studio": "HBO",
"subtitle_codec": "", "subtitle_codec": "",
@@ -560,12 +564,14 @@ Returns:
"video_decision": "direct play", "video_decision": "direct play",
"video_frame_rate": "23.976", "video_frame_rate": "23.976",
"video_framerate": "24p", "video_framerate": "24p",
"video_full_resolution": "1080p",
"video_height": "1078", "video_height": "1078",
"video_language": "", "video_language": "",
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_resolution": "1080", "video_resolution": "1080",
"video_scan_type": "progressive",
"video_width": "1920", "video_width": "1920",
"view_offset": "1000", "view_offset": "1000",
"width": "1920", "width": "1920",
@@ -700,8 +706,9 @@ Returns:
"parent_title": "", "parent_title": "",
"paused_counter": 0, "paused_counter": 0,
"percent_complete": 84, "percent_complete": 84,
"platform": "Chrome", "platform": "Windows",
"player": "Plex Web (Chrome)", "product": "Plex for Windows",
"player": "Castle-PC",
"rating_key": 4348, "rating_key": 4348,
"reference_id": 1123, "reference_id": 1123,
"session_key": null, "session_key": null,
@@ -833,6 +840,7 @@ Required parameters:
None None
Optional parameters: Optional parameters:
grouping (int): 0 or 1
order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count", order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count",
"child_count", "last_accessed", "last_played", "plays", "duration" "child_count", "last_accessed", "last_played", "plays", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
@@ -1108,6 +1116,7 @@ Returns:
"Drama", "Drama",
"Fantasy" "Fantasy"
], ],
"grandparent_guid": "com.plexapp.agents.thetvdb://121361?lang=en",
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
@@ -1148,6 +1157,7 @@ Returns:
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_scan_type": "progressive",
"video_width": "1920", "video_width": "1920",
"selected": 0 "selected": 0
}, },
@@ -1182,6 +1192,7 @@ Returns:
], ],
"video_codec": "h264", "video_codec": "h264",
"video_framerate": "24p", "video_framerate": "24p",
"video_full_resolution": "1080p",
"video_profile": "high", "video_profile": "high",
"video_resolution": "1080", "video_resolution": "1080",
"width": "1920" "width": "1920"
@@ -1190,6 +1201,7 @@ Returns:
"media_type": "episode", "media_type": "episode",
"original_title": "", "original_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -2341,6 +2353,7 @@ Required parameters:
None None
Optional parameters: Optional parameters:
grouping (int): 0 or 1
order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform", order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform",
"player", "last_played", "plays", "duration" "player", "last_played", "plays", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"

View File

@@ -1,5 +1,73 @@
# Changelog # Changelog
## v2.1.36-beta (2019-10-05)
* Monitoring:
* Fix: Activity card title not updating after pre-rolls or auto-play.
* History:
* Fix: Display correct interlaced or progressive video scan type on stream data modal.
* Graphs:
* New: Separate interlaced and progressive video scan type on source and stream resolution graphs.
* API:
* New: Added parent_guid and grandparent_guid to get_activity and get_metadata commands.
## v2.1.35-beta (2019-09-24)
* Monitoring:
* Fix: Audio shown as blank on activity cards when changing audio tracks during direct play.
* Fix: Display correct interlaced or progressive video scan type on activity cards.
* New: Added flag for Nvidia hardware decoding on activity cards.
* Notifications:
* Fix: Notification parameter prefix and suffix were not substituted correctly.
* Fix: Release Date notification parameter was incorrectly casted to an integer instead of a string.
* New: Added video scan type and full resolution notification parameters.
* UI:
* Fix: Movies with the same title but different year being grouped on the homepage stats cards.
* API:
* New: Added video scan type and full resolution values to get_activity command.
* Other:
* Fix: Tautulli logging out every time after saving settings and restarting.
## v2.1.34 (2019-09-03)
* History:
* New: Added Product column to history tables.
* Notifications:
* Fix: IMDB/TMDb/TVDB/TVmaze ID notification parameters showing blank values after lookup.
* UI:
* Fix: Libraries and Users tables did not respect the group history setting.
* API:
* Fix: Title field was not searchable in get_library_media_info command.
* New: Added grouping option to get_libraries_table and get_users_table commands.
* New: Added product value to get_history command.
* Other:
* Fix: Could not verify Plex Media Server with unpublished hostnames.
* Change: Automatically logout all Tautulli instances when changing the admin password.
## v2.1.33 (2019-07-27)
* Notifications:
* Change: Mask notification agent password fields.
* Change: Enable searching by email address in dropdown menu.
* Other:
* Fix: Version number being overwritten with "None" which prevented updating in some instances.
* Change: Update Plex OAuth request headers.
## v2.1.32 (2019-06-26)
* Newsletters:
* Fix: Newsletter scheduler issue for QNAP devices using an invalid "local" timezone preventing Tautulli from starting.
## v2.1.31 (2019-06-24)
* No additional changes from v2.1.31-beta.
## v2.1.31-beta (2019-06-13) ## v2.1.31-beta (2019-06-13)
* Monitoring: * Monitoring:
@@ -23,6 +91,7 @@
## v2.1.29 (2019-05-11) ## v2.1.29 (2019-05-11)
* No additional changes from v2.1.29-beta. * No additional changes from v2.1.29-beta.
@@ -167,6 +236,7 @@
## v2.1.20 (2018-09-05) ## v2.1.20 (2018-09-05)
* No additional changes from v2.1.20-beta. * No additional changes from v2.1.20-beta.

View File

@@ -69,7 +69,7 @@ DOCUMENTATION :: END
% endif % endif
<tr> <tr>
<td>Platform:</td> <td>Platform:</td>
<td>${common.PLATFORM} ${common.PLATFORM_RELEASE} (${common.PLATFORM_VERSION + (' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else '')})</td> <td>${'[Docker] ' if plexpy.DOCKER else ''}${common.PLATFORM} ${common.PLATFORM_RELEASE} (${common.PLATFORM_VERSION + (' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else '')})</td>
</tr> </tr>
<tr> <tr>
<td>System Timezone:</td> <td>System Timezone:</td>

View File

@@ -232,11 +232,11 @@ DOCUMENTATION :: END
hw_d = ' (HW)' if data['transcode_hw_decoding'] else '' hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
hw_e = ' (HW)' if data['transcode_hw_encoding'] else '' hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
%> %>
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}) Transcode (${data['video_codec'].upper()}${hw_d} ${data['video_full_resolution']} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${data['stream_video_full_resolution']})
% elif data['stream_video_decision'] == 'copy': % elif data['stream_video_decision'] == 'copy':
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}) Direct Stream (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']})
% else: % else:
Direct Play (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}) Direct Play (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']})
% endif % endif
% elif data['media_type'] == 'photo': % elif data['media_type'] == 'photo':
Direct Play (${data['width']}x${data['height']}) Direct Play (${data['width']}x${data['height']})

View File

@@ -60,7 +60,8 @@
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="device">Player</th> <th align="left" id="product">Product</th>
<th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
<th align="left" id="paused_counter">Paused</th> <th align="left" id="paused_counter">Paused</th>
@@ -143,7 +144,7 @@
var colvis = new $.fn.dataTable.ColVis(history_table, { var colvis = new $.fn.dataTable.ColVis(history_table, {
buttonText: '<i class="fa fa-columns"></i> Select columns', buttonText: '<i class="fa fa-columns"></i> Select columns',
buttonClass: 'btn btn-dark', buttonClass: 'btn btn-dark',
exclude: [0, 11] exclude: [0, 12]
}); });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');

View File

@@ -26,6 +26,7 @@
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="device">Player</th> <th align="left" id="device">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -61,7 +62,7 @@
}; };
history_table = $('#history_table_modal').DataTable(history_table_options); history_table = $('#history_table_modal').DataTable(history_table_options);
history_table.columns([0, 3, 4, 8, 10, 11]).visible(false); history_table.columns([0, 3, 4, 5, 9, 11, 12]).visible(false);
clearSearchButton('history_table_modal', history_table); clearSearchButton('history_table_modal', history_table);

View File

@@ -355,8 +355,9 @@
var session_id = s.session_id; var session_id = s.session_id;
var instance = $('#activity-instance-' + key); var instance = $('#activity-instance-' + key);
// Create a new instance if it doesn't exist // Create a new instance if it doesn't exist or recreate the entire instance
if (!(instance.length)) { // if the rating key changed (for movies or episodes) with the same session key
if (!(instance.length) || (s.media_type !== 'track' && s.rating_key !== instance.data('rating_key').toString())) {
create_instances.push(key); create_instances.push(key);
getActivityInstance(key); getActivityInstance(key);
return; return;
@@ -382,7 +383,7 @@
// Switching tracks can be under the same session key, so need to update the info. // Switching tracks can be under the same session key, so need to update the info.
if (s.media_type === 'track') { if (s.media_type === 'track') {
// Update if artist changed // Update if artist changed
if (s.grandparent_rating_key !== instance.data('grandparent_rating_key')) { if (s.grandparent_rating_key !== instance.data('grandparent_rating_key').toString()) {
$('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true)'); $('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true)');
$('#metadata-grandparent_title-' + key) $('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key) .attr('href', 'info?rating_key=' + s.grandparent_rating_key)
@@ -390,7 +391,7 @@
.text(s.original_title || s.grandparent_title); .text(s.original_title || s.grandparent_title);
} }
// Update cover if album changed // Update cover if album changed
if (s.parent_rating_key !== instance.data('parent_rating_key')) { if (s.parent_rating_key !== instance.data('parent_rating_key').toString()) {
$('#poster-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)'); $('#poster-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
$('#poster-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&opacity=60&background=282828&blur=3&fallback=poster&refresh=true)'); $('#poster-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&opacity=60&background=282828&blur=3&fallback=poster&refresh=true)');
$('#poster-url-' + key) $('#poster-url-' + key)
@@ -402,7 +403,7 @@
.text(s.parent_title); .text(s.parent_title);
} }
// Update cover if track changed // Update cover if track changed
if (s.rating_key !== instance.data('rating_key')) { if (s.rating_key !== instance.data('rating_key').toString()) {
$('#metadata-grandparent_title-' + key) $('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key) .attr('href', 'info?rating_key=' + s.grandparent_rating_key)
.attr('title', s.original_title || s.grandparent_title) .attr('title', s.original_title || s.grandparent_title)
@@ -445,7 +446,7 @@
v_res = '4k'; v_res = '4k';
break; break;
default: default:
v_res = s.video_resolution + 'p' v_res = s.video_full_resolution;
} }
var sv_res = ''; var sv_res = '';
switch (s.stream_video_resolution.toLowerCase()) { switch (s.stream_video_resolution.toLowerCase()) {
@@ -456,7 +457,7 @@
sv_res = '4k'; sv_res = '4k';
break; break;
default: default:
sv_res = s.stream_video_resolution + 'p' sv_res = s.stream_video_full_resolution;
} }
if (s.stream_video_decision === 'transcode') { if (s.stream_video_decision === 'transcode') {
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : ''; var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
@@ -580,7 +581,14 @@
session_key: session_key session_key: session_key
}, },
complete: function(xhr, status) { complete: function(xhr, status) {
var instance = $('#activity-instance-' + session_key);
if (instance.length) {
instance.replaceWith(xhr.responseText);
} else {
$('#currentActivity').append(xhr.responseText); $('#currentActivity').append(xhr.responseText);
}
$('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar(); $('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar();
$('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 }); $('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 });
$('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 }); $('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });

View File

@@ -451,6 +451,7 @@ DOCUMENTATION :: END
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="player">Player</th> <th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -613,7 +614,7 @@ DOCUMENTATION :: END
$(document).ready(function () { $(document).ready(function () {
get_history(); get_history();
history_table = $('#history_table-RK-${data["rating_key"]}').DataTable(history_table_options); history_table = $('#history_table-RK-${data["rating_key"]}').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 12] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table-RK-${data["rating_key"]}', history_table); clearSearchButton('history_table-RK-${data["rating_key"]}', history_table);

View File

@@ -568,8 +568,11 @@ function getPlexHeaders() {
'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false), 'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false),
'X-Plex-Platform': p.name, 'X-Plex-Platform': p.name,
'X-Plex-Platform-Version': p.version, 'X-Plex-Platform-Version': p.version,
'X-Plex-Model': 'Plex OAuth',
'X-Plex-Device': p.os, 'X-Plex-Device': p.os,
'X-Plex-Device-Name': p.name 'X-Plex-Device-Name': p.name,
'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height,
'X-Plex-Language': 'en'
}; };
} }
@@ -655,7 +658,21 @@ function PlexOAuth(success, error, pre) {
const pin = data.pin; const pin = data.pin;
const code = data.code; const code = data.code;
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?clientID=' + x_plex_headers['X-Plex-Client-Identifier'] + '&code=' + code; var oauth_params = {
'clientID': x_plex_headers['X-Plex-Client-Identifier'],
'context[device][product]': x_plex_headers['X-Plex-Product'],
'context[device][version]': x_plex_headers['X-Plex-Version'],
'context[device][platform]': x_plex_headers['X-Plex-Platform'],
'context[device][platformVersion]': x_plex_headers['X-Plex-Platform-Version'],
'context[device][device]': x_plex_headers['X-Plex-Device'],
'context[device][deviceName]': x_plex_headers['X-Plex-Device-Name'],
'context[device][model]': x_plex_headers['X-Plex-Model'],
'context[device][screenResolution]': x_plex_headers['X-Plex-Device-Screen-Resolution'],
'context[device][layout]': 'desktop',
'code': code
}
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?' + encodeData(oauth_params);
polling = pin; polling = pin;
(function poll() { (function poll() {
@@ -694,3 +711,9 @@ function PlexOAuth(success, error, pre) {
} }
}); });
} }
function encodeData(data) {
return Object.keys(data).map(function(key) {
return [key, data[key]].map(encodeURIComponent).join("=");
}).join("&");
}

View File

@@ -115,7 +115,7 @@ history_table_options = {
"data": "platform", "data": "platform",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
$(td).html(cellData); $(td).html(capitalizeFirstLetter(cellData));
} }
}, },
"width": "10%", "width": "10%",
@@ -123,6 +123,17 @@ history_table_options = {
}, },
{ {
"targets": [5], "targets": [5],
"data": "product",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "10%",
"className": "no-wrap"
},
{
"targets": [6],
"data": "player", "data": "player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@@ -137,11 +148,11 @@ history_table_options = {
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>'); $(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>');
} }
}, },
"width": "12%", "width": "10%",
"className": "no-wrap modal-control" "className": "no-wrap modal-control"
}, },
{ {
"targets": [6], "targets": [7],
"data": "full_title", "data": "full_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@@ -171,11 +182,11 @@ history_table_options = {
} }
} }
}, },
"width": "33%", "width": "25%",
"className": "datatable-wrap" "className": "datatable-wrap"
}, },
{ {
"targets": [7], "targets": [8],
"data": "started", "data": "started",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@@ -189,7 +200,7 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [8], "targets": [9],
"data": "paused_counter", "data": "paused_counter",
"render": function (data, type, full) { "render": function (data, type, full) {
if (data !== null) { if (data !== null) {
@@ -203,7 +214,7 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [9], "targets": [10],
"data": "stopped", "data": "stopped",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null || (rowData['state'] != null && rowData['state'] != "stopped")) { if (cellData === null || (rowData['state'] != null && rowData['state'] != "stopped")) {
@@ -217,7 +228,7 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [10], "targets": [11],
"data": "duration", "data": "duration",
"render": function (data, type, full) { "render": function (data, type, full) {
if (data !== null) { if (data !== null) {
@@ -231,7 +242,7 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [11], "targets": [12],
"data": "watched_status", "data": "watched_status",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData == 1) { if (cellData == 1) {
@@ -489,7 +500,8 @@ function childTableFormat(rowData) {
'<th align="left" id="friendly_name">User</th>' + '<th align="left" id="friendly_name">User</th>' +
'<th align="left" id="ip_address">IP Address</th>' + '<th align="left" id="ip_address">IP Address</th>' +
'<th align="left" id="platform">Platform</th>' + '<th align="left" id="platform">Platform</th>' +
'<th align="left" id="platform">Player</th>' + '<th align="left" id="product">Product</th>' +
'<th align="left" id="player">Player</th>' +
'<th align="left" id="title">Title</th>' + '<th align="left" id="title">Title</th>' +
'<th align="left" id="started">Started</th>' + '<th align="left" id="started">Started</th>' +
'<th align="left" id="paused_counter">Paused</th>' + '<th align="left" id="paused_counter">Paused</th>' +

View File

@@ -50,7 +50,7 @@ media_info_table_options = {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Episodes"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Episodes"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'artist') { } else if (rowData['media_type'] === 'artist') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Albumns"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Albums"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'album') { } else if (rowData['media_type'] === 'album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';

View File

@@ -205,6 +205,7 @@ DOCUMENTATION :: END
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="player">Player</th> <th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -385,7 +386,7 @@ DOCUMENTATION :: END
}; };
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options); history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 12] });
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-SID-${data["section_id"]}', history_table); clearSearchButton('history_table-SID-${data["section_id"]}', history_table);

View File

@@ -584,6 +584,7 @@
var $email_selectors = $('#newsletter_email_to, #newsletter_email_cc, #newsletter_email_bcc').selectize({ var $email_selectors = $('#newsletter_email_to, #newsletter_email_cc, #newsletter_email_bcc').selectize({
plugins: ['remove_button'], plugins: ['remove_button'],
maxItems: null, maxItems: null,
searchField: ['text', 'value'],
render: { render: {
item: function(item, escape) { item: function(item, escape) {
return '<div>' + return '<div>' +

View File

@@ -566,6 +566,7 @@
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({ var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
plugins: ['remove_button'], plugins: ['remove_button'],
maxItems: null, maxItems: null,
searchField: ['text', 'value'],
render: { render: {
item: function(item, escape) { item: function(item, escape) {
return '<div>' + return '<div>' +

View File

@@ -96,8 +96,8 @@ DOCUMENTATION :: END
% if data['media_type'] != 'track': % if data['media_type'] != 'track':
<tr> <tr>
<td>Resolution</td> <td>Resolution</td>
<td>${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}</td> <td>${data['stream_video_full_resolution']}</td>
<td>${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])}</td> <td>${data['video_full_resolution']}</td>
</tr> </tr>
% endif % endif
<tr> <tr>

View File

@@ -184,6 +184,7 @@ DOCUMENTATION :: END
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="player">Player</th> <th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -425,7 +426,7 @@ DOCUMENTATION :: END
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options); history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
history_table.column(2).visible(false); history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 12] });
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-UID-${data["user_id"]}', history_table); clearSearchButton('history_table-UID-${data["user_id"]}', history_table);

View File

@@ -158,8 +158,8 @@ def initialize(config_file):
logger.info(u"Starting Tautulli {}".format( logger.info(u"Starting Tautulli {}".format(
common.RELEASE common.RELEASE
)) ))
logger.info(u"{} {} ({}{})".format( logger.info(u"{}{} {} ({}{})".format(
common.PLATFORM, common.PLATFORM_RELEASE, common.PLATFORM_VERSION, '[Docker] ' if DOCKER else '', common.PLATFORM, common.PLATFORM_RELEASE, common.PLATFORM_VERSION,
' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else '' ' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else ''
)) ))
logger.info(u"{} (UTC{})".format( logger.info(u"{} (UTC{})".format(
@@ -217,9 +217,10 @@ def initialize(config_file):
CONFIG.write() CONFIG.write()
# Check if Tautulli has a jwt_secret # Check if Tautulli has a jwt_secret
if CONFIG.JWT_SECRET == '' or not CONFIG.JWT_SECRET: if CONFIG.JWT_SECRET == '' or not CONFIG.JWT_SECRET or CONFIG.JWT_UPDATE_SECRET:
logger.debug(u"Generating JWT secret...") logger.debug(u"Generating JWT secret...")
CONFIG.JWT_SECRET = generate_uuid() CONFIG.JWT_SECRET = generate_uuid()
CONFIG.JWT_UPDATE_SECRET = False
CONFIG.write() CONFIG.write()
# Get the previous version from the file # Get the previous version from the file
@@ -253,7 +254,7 @@ def initialize(config_file):
# Check for new versions # Check for new versions
if CONFIG.CHECK_GITHUB_ON_STARTUP and CONFIG.CHECK_GITHUB: if CONFIG.CHECK_GITHUB_ON_STARTUP and CONFIG.CHECK_GITHUB:
try: try:
LATEST_VERSION = versioncheck.check_update() versioncheck.check_update()
except: except:
logger.exception(u"Unhandled exception") logger.exception(u"Unhandled exception")
LATEST_VERSION = CURRENT_VERSION LATEST_VERSION = CURRENT_VERSION
@@ -581,12 +582,12 @@ def dbcheck():
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, ' 'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
'transcode_decision TEXT, container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, ' 'transcode_decision TEXT, container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, '
'video_codec TEXT, video_bitrate INTEGER, video_resolution TEXT, video_width INTEGER, video_height INTEGER, ' 'video_codec TEXT, video_bitrate INTEGER, video_resolution TEXT, video_width INTEGER, video_height INTEGER, '
'video_framerate TEXT, aspect_ratio TEXT, ' 'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
'audio_codec TEXT, audio_bitrate INTEGER, audio_channels INTEGER, subtitle_codec TEXT, ' 'audio_codec TEXT, audio_bitrate INTEGER, audio_channels INTEGER, subtitle_codec TEXT, '
'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile TEXT, ' 'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile TEXT, '
'stream_container_decision TEXT, stream_container TEXT, ' 'stream_container_decision TEXT, stream_container TEXT, '
'stream_video_decision TEXT, stream_video_codec TEXT, stream_video_bitrate INTEGER, stream_video_width INTEGER, ' 'stream_video_decision TEXT, stream_video_codec TEXT, stream_video_bitrate INTEGER, stream_video_width INTEGER, '
'stream_video_height INTEGER, stream_video_framerate TEXT, ' 'stream_video_height INTEGER, stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, ' 'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, ' 'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, '
'transcode_protocol TEXT, transcode_container TEXT, ' 'transcode_protocol TEXT, transcode_container TEXT, '
@@ -616,7 +617,7 @@ def dbcheck():
'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, ' 'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, '
'container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, video_bitrate INTEGER, video_bit_depth INTEGER, ' 'container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, video_bitrate INTEGER, video_bit_depth INTEGER, '
'video_codec TEXT, video_codec_level TEXT, video_width INTEGER, video_height INTEGER, video_resolution TEXT, ' 'video_codec TEXT, video_codec_level TEXT, video_width INTEGER, video_height INTEGER, video_resolution TEXT, '
'video_framerate TEXT, aspect_ratio TEXT, ' 'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, ' 'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, ' 'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, ' 'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
@@ -626,7 +627,7 @@ def dbcheck():
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, ' 'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, ' 'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, ' 'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
'stream_video_framerate TEXT, ' 'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, ' 'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, ' 'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, '
'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, ' 'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, '
@@ -1174,6 +1175,24 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN rating_key_websocket TEXT' 'ALTER TABLE sessions ADD COLUMN rating_key_websocket TEXT'
) )
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT video_scan_type FROM sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN video_scan_type TEXT'
)
c_db.execute(
'ALTER TABLE sessions ADD COLUMN video_full_resolution TEXT'
)
c_db.execute(
'ALTER TABLE sessions ADD COLUMN stream_video_scan_type TEXT'
)
c_db.execute(
'ALTER TABLE sessions ADD COLUMN stream_video_full_resolution TEXT'
)
# Upgrade session_history table from earlier versions # Upgrade session_history table from earlier versions
try: try:
c_db.execute('SELECT reference_id FROM session_history') c_db.execute('SELECT reference_id FROM session_history')
@@ -1473,6 +1492,45 @@ def dbcheck():
except sqlite3.OperationalError: except sqlite3.OperationalError:
logger.warn(u"Unable to remove NULL values from session_history_media_info table.") logger.warn(u"Unable to remove NULL values from session_history_media_info table.")
# Upgrade session_history_media_info table from earlier versions
try:
c_db.execute('SELECT video_scan_type FROM session_history_media_info')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table session_history_media_info.")
c_db.execute(
'ALTER TABLE session_history_media_info ADD COLUMN video_scan_type TEXT'
)
c_db.execute(
'ALTER TABLE session_history_media_info ADD COLUMN video_full_resolution TEXT'
)
c_db.execute(
'ALTER TABLE session_history_media_info ADD COLUMN stream_video_scan_type TEXT'
)
c_db.execute(
'ALTER TABLE session_history_media_info ADD COLUMN stream_video_full_resolution TEXT'
)
c_db.execute(
'UPDATE session_history_media_info SET video_scan_type = "progressive" '
'WHERE video_resolution != ""'
)
c_db.execute(
'UPDATE session_history_media_info SET stream_video_scan_type = "progressive" '
'WHERE stream_video_resolution != "" AND stream_video_resolution IS NOT NULL'
)
c_db.execute(
'UPDATE session_history_media_info SET video_full_resolution = (CASE '
'WHEN video_resolution = "" OR video_resolution = "SD" OR video_resolution = "4k" THEN video_resolution '
'WHEN video_resolution = "sd" THEN "SD" '
'ELSE video_resolution || "p" END)'
)
c_db.execute(
'UPDATE session_history_media_info SET stream_video_full_resolution = ( '
'CASE WHEN stream_video_resolution = "" OR stream_video_resolution = "SD" OR stream_video_resolution = "4k" '
'THEN stream_video_resolution '
'WHEN stream_video_resolution = "sd" THEN "SD" '
'ELSE stream_video_resolution || "p" END)'
)
# Upgrade users table from earlier versions # Upgrade users table from earlier versions
try: try:
c_db.execute('SELECT do_notify FROM users') c_db.execute('SELECT do_notify FROM users')

View File

@@ -17,13 +17,13 @@ import threading
import time import time
import plexpy import plexpy
import activity_handler
import activity_processor import activity_processor
import database import database
import helpers import helpers
import libraries import libraries
import logger import logger
import notification_handler import notification_handler
import notifiers
import plextv import plextv
import pmsconnect import pmsconnect
import web_socket import web_socket
@@ -37,17 +37,23 @@ int_ping_count = 0
def check_active_sessions(ws_request=False): def check_active_sessions(ws_request=False):
with monitor_lock: with monitor_lock:
pms_connect = pmsconnect.PmsConnect()
session_list = pms_connect.get_current_activity()
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
monitor_process = activity_processor.ActivityProcessor() monitor_process = activity_processor.ActivityProcessor()
db_streams = monitor_process.get_sessions()
# Clear the metadata cache
for stream in db_streams:
activity_handler.delete_metadata_cache(stream['session_key'])
pms_connect = pmsconnect.PmsConnect()
session_list = pms_connect.get_current_activity()
logger.debug(u"Tautulli Monitor :: Checking for active streams.") logger.debug(u"Tautulli Monitor :: Checking for active streams.")
if session_list: if session_list:
media_container = session_list['sessions'] media_container = session_list['sessions']
# Check our temp table for what we must do with the new streams # Check our temp table for what we must do with the new streams
db_streams = monitor_process.get_sessions()
for stream in db_streams: for stream in db_streams:
if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key']) if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key'])
for d in media_container): for d in media_container):

View File

@@ -76,6 +76,8 @@ class ActivityProcessor(object):
'video_height': session.get('video_height', ''), 'video_height': session.get('video_height', ''),
'video_resolution': session.get('video_resolution', ''), 'video_resolution': session.get('video_resolution', ''),
'video_framerate': session.get('video_framerate', ''), 'video_framerate': session.get('video_framerate', ''),
'video_scan_type': session.get('video_scan_type', ''),
'video_full_resolution': session.get('video_full_resolution', ''),
'aspect_ratio': session.get('aspect_ratio', ''), 'aspect_ratio': session.get('aspect_ratio', ''),
'audio_codec': session.get('audio_codec', ''), 'audio_codec': session.get('audio_codec', ''),
'audio_bitrate': session.get('audio_bitrate', ''), 'audio_bitrate': session.get('audio_bitrate', ''),
@@ -106,6 +108,8 @@ class ActivityProcessor(object):
'stream_video_width': session.get('stream_video_width', ''), 'stream_video_width': session.get('stream_video_width', ''),
'stream_video_height': session.get('stream_video_height', ''), 'stream_video_height': session.get('stream_video_height', ''),
'stream_video_framerate': session.get('stream_video_framerate', ''), 'stream_video_framerate': session.get('stream_video_framerate', ''),
'stream_video_scan_type': session.get('stream_video_scan_type', ''),
'stream_video_full_resolution': session.get('stream_video_full_resolution', ''),
'stream_audio_decision': session.get('stream_audio_decision', ''), 'stream_audio_decision': session.get('stream_audio_decision', ''),
'stream_audio_codec': session.get('stream_audio_codec', ''), 'stream_audio_codec': session.get('stream_audio_codec', ''),
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''), 'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
@@ -347,6 +351,8 @@ class ActivityProcessor(object):
'video_height': session['video_height'], 'video_height': session['video_height'],
'video_resolution': session['video_resolution'], 'video_resolution': session['video_resolution'],
'video_framerate': session['video_framerate'], 'video_framerate': session['video_framerate'],
'video_scan_type': session['video_scan_type'],
'video_full_resolution': session['video_full_resolution'],
'aspect_ratio': session['aspect_ratio'], 'aspect_ratio': session['aspect_ratio'],
'audio_codec': session['audio_codec'], 'audio_codec': session['audio_codec'],
'audio_bitrate': session['audio_bitrate'], 'audio_bitrate': session['audio_bitrate'],
@@ -379,6 +385,8 @@ class ActivityProcessor(object):
'stream_video_width': session['stream_video_width'], 'stream_video_width': session['stream_video_width'],
'stream_video_resolution': session['stream_video_resolution'], 'stream_video_resolution': session['stream_video_resolution'],
'stream_video_framerate': session['stream_video_framerate'], 'stream_video_framerate': session['stream_video_framerate'],
'stream_video_scan_type': session['stream_video_scan_type'],
'stream_video_full_resolution': session['stream_video_full_resolution'],
'stream_audio_decision': session['stream_audio_decision'], 'stream_audio_decision': session['stream_audio_decision'],
'stream_audio_codec': session['stream_audio_codec'], 'stream_audio_codec': session['stream_audio_codec'],
'stream_audio_bitrate': session['stream_audio_bitrate'], 'stream_audio_bitrate': session['stream_audio_bitrate'],

View File

@@ -119,11 +119,6 @@ AUDIO_CODEC_OVERRIDES = {
VIDEO_RESOLUTION_OVERRIDES = { VIDEO_RESOLUTION_OVERRIDES = {
'sd': 'SD', 'sd': 'SD',
'480': '480p',
'540': '540p',
'576': '576p',
'720': '720p',
'1080': '1080p',
'4k': '4k' '4k': '4k'
} }
@@ -168,11 +163,11 @@ HW_DECODERS = [
'dxva2', 'dxva2',
'videotoolbox', 'videotoolbox',
'mediacodecndk', 'mediacodecndk',
'vaapi' 'vaapi',
'nvdec'
] ]
HW_ENCODERS = [ HW_ENCODERS = [
'qsv', 'qsv',
'nvenc',
'mf', 'mf',
'videotoolbox', 'videotoolbox',
'mediacodecndk', 'mediacodecndk',
@@ -366,8 +361,10 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'}, {'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'},
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'}, {'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'},
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'}, {'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'},
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'},
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'}, {'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'},
{'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution', 'description': 'The video resolution of the stream.'}, {'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution', 'description': 'The video resolution of the stream.'},
{'name': 'Stream Video Scan Type', 'type': 'str', 'value': 'stream_video_scan_type', 'description': 'The video scan type of the stream.'},
{'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height', 'description': 'The video height of the stream.'}, {'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height', 'description': 'The video height of the stream.'},
{'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width', 'description': 'The video width of the stream.'}, {'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width', 'description': 'The video width of the stream.'},
{'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language', 'description': 'The video language of the stream.'}, {'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language', 'description': 'The video language of the stream.'},
@@ -430,7 +427,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'}, {'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'}, {'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'}, {'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
{'name': 'Release Date', 'type': 'int', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'}, {'name': 'Release Date', 'type': 'str', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'},
{'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'}, {'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'},
{'name': 'Added Date', 'type': 'str', 'value': 'added_date', 'description': 'The date (in date format) the item was added to Plex.'}, {'name': 'Added Date', 'type': 'str', 'value': 'added_date', 'description': 'The date (in date format) the item was added to Plex.'},
{'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'}, {'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'},
@@ -469,8 +466,10 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'}, {'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'},
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'}, {'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'},
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'}, {'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'},
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'},
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'}, {'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'},
{'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution', 'description': 'The video resolution of the original media.'}, {'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution', 'description': 'The video resolution of the original media.'},
{'name': 'Video Scan Tpye', 'type': 'str', 'value': 'video_scan_type', 'description': 'The video scan type of the original media.'},
{'name': 'Video Height', 'type': 'int', 'value': 'video_height', 'description': 'The video height of the original media.'}, {'name': 'Video Height', 'type': 'int', 'value': 'video_height', 'description': 'The video height of the original media.'},
{'name': 'Video Width', 'type': 'int', 'value': 'video_width', 'description': 'The video width of the original media.'}, {'name': 'Video Width', 'type': 'int', 'value': 'video_width', 'description': 'The video width of the original media.'},
{'name': 'Video Language', 'type': 'str', 'value': 'video_language', 'description': 'The video language of the original media.'}, {'name': 'Video Language', 'type': 'str', 'value': 'video_language', 'description': 'The video language of the original media.'},

View File

@@ -624,6 +624,7 @@ _CONFIG_DEFINITIONS = {
'XBMC_ON_CONCURRENT': (int, 'XBMC', 0), 'XBMC_ON_CONCURRENT': (int, 'XBMC', 0),
'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0), 'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0),
'JWT_SECRET': (str, 'Advanced', ''), 'JWT_SECRET': (str, 'Advanced', ''),
'JWT_UPDATE_SECRET': (bool_int, 'Advanced', 0),
'SYSTEM_ANALYTICS': (int, 'Advanced', 1), 'SYSTEM_ANALYTICS': (int, 'Advanced', 1),
'WIN_SYS_TRAY': (int, 'General', 1) 'WIN_SYS_TRAY': (int, 'General', 1)
} }

View File

@@ -76,6 +76,7 @@ class DataFactory(object):
'(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" \ '(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" \
THEN users.username ELSE users.friendly_name END) AS friendly_name', THEN users.username ELSE users.friendly_name END) AS friendly_name',
'platform', 'platform',
'product',
'player', 'player',
'ip_address', 'ip_address',
'session_history.media_type', 'session_history.media_type',
@@ -123,6 +124,7 @@ class DataFactory(object):
'(CASE WHEN friendly_name IS NULL OR TRIM(friendly_name) = "" \ '(CASE WHEN friendly_name IS NULL OR TRIM(friendly_name) = "" \
THEN user ELSE friendly_name END) AS friendly_name', THEN user ELSE friendly_name END) AS friendly_name',
'platform', 'platform',
'product',
'player', 'player',
'ip_address', 'ip_address',
'media_type', 'media_type',
@@ -225,6 +227,7 @@ class DataFactory(object):
'user': item['user'], 'user': item['user'],
'friendly_name': item['friendly_name'], 'friendly_name': item['friendly_name'],
'platform': platform, 'platform': platform,
'product': item['product'],
'player': item['player'], 'player': item['player'],
'ip_address': item['ip_address'], 'ip_address': item['ip_address'],
'media_type': item['media_type'], 'media_type': item['media_type'],
@@ -294,7 +297,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \ ' >= datetime("now", "-%s days", "localtime") ' \
' AND session_history.media_type = "movie" ' \ ' AND session_history.media_type = "movie" ' \
' GROUP BY %s) AS t ' \ ' GROUP BY %s) AS t ' \
'GROUP BY t.full_title ' \ 'GROUP BY t.full_title, t.year ' \
'ORDER BY %s DESC, started DESC ' \ 'ORDER BY %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count) 'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
@@ -345,7 +348,7 @@ class DataFactory(object):
' >= datetime("now", "-%s days", "localtime") ' \ ' >= datetime("now", "-%s days", "localtime") ' \
' AND session_history.media_type = "movie" ' \ ' AND session_history.media_type = "movie" ' \
' GROUP BY %s) AS t ' \ ' GROUP BY %s) AS t ' \
'GROUP BY t.full_title ' \ 'GROUP BY t.full_title, t.year ' \
'ORDER BY users_watched DESC, %s DESC, started DESC ' \ 'ORDER BY users_watched DESC, %s DESC, started DESC ' \
'LIMIT %s ' % (time_range, group_by, sort_type, stats_count) 'LIMIT %s ' % (time_range, group_by, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
@@ -872,12 +875,12 @@ class DataFactory(object):
user_cond = 'AND %s.user_id = %s ' % (table, session.get_session_user_id()) user_cond = 'AND %s.user_id = %s ' % (table, session.get_session_user_id())
if row_id: if row_id:
query = 'SELECT bitrate, video_resolution, ' \ query = 'SELECT bitrate, video_full_resolution, ' \
'optimized_version, optimized_version_profile, optimized_version_title, ' \ 'optimized_version, optimized_version_profile, optimized_version_title, ' \
'synced_version, synced_version_profile, ' \ 'synced_version, synced_version_profile, ' \
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \ 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \ 'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
'stream_bitrate, stream_video_resolution, quality_profile, stream_container_decision, stream_container, ' \ 'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \ 'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
'stream_video_framerate, ' \ 'stream_video_framerate, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \ 'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
@@ -893,12 +896,12 @@ class DataFactory(object):
'WHERE session_history_media_info.id = ? %s' % user_cond 'WHERE session_history_media_info.id = ? %s' % user_cond
result = monitor_db.select(query, args=[row_id]) result = monitor_db.select(query, args=[row_id])
elif session_key: elif session_key:
query = 'SELECT bitrate, video_resolution, ' \ query = 'SELECT bitrate, video_full_resolution, ' \
'optimized_version, optimized_version_profile, optimized_version_title, ' \ 'optimized_version, optimized_version_profile, optimized_version_title, ' \
'synced_version, synced_version_profile, ' \ 'synced_version, synced_version_profile, ' \
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \ 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \ 'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
'stream_bitrate, stream_video_resolution, quality_profile, stream_container_decision, stream_container, ' \ 'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \ 'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
'stream_video_framerate, ' \ 'stream_video_framerate, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \ 'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
@@ -921,7 +924,7 @@ class DataFactory(object):
# For backwards compatibility. Pick one new Tautulli key to check and override with old values. # For backwards compatibility. Pick one new Tautulli key to check and override with old values.
if not item['stream_container']: if not item['stream_container']:
item['stream_video_resolution'] = item['video_resolution'] item['stream_video_full_resolution'] = item['video_full_resolution']
item['stream_container'] = item['transcode_container'] or item['container'] item['stream_container'] = item['transcode_container'] or item['container']
item['stream_video_decision'] = item['video_decision'] item['stream_video_decision'] = item['video_decision']
item['stream_video_codec'] = item['transcode_video_codec'] or item['video_codec'] item['stream_video_codec'] = item['transcode_video_codec'] or item['video_codec']
@@ -935,7 +938,7 @@ class DataFactory(object):
pre_tautulli = 1 pre_tautulli = 1
stream_output = {'bitrate': item['bitrate'], stream_output = {'bitrate': item['bitrate'],
'video_resolution': item['video_resolution'], 'video_full_resolution': item['video_full_resolution'],
'optimized_version': item['optimized_version'], 'optimized_version': item['optimized_version'],
'optimized_version_profile': item['optimized_version_profile'], 'optimized_version_profile': item['optimized_version_profile'],
'optimized_version_title': item['optimized_version_title'], 'optimized_version_title': item['optimized_version_title'],
@@ -953,7 +956,7 @@ class DataFactory(object):
'audio_channels': item['audio_channels'], 'audio_channels': item['audio_channels'],
'subtitle_codec': item['subtitle_codec'], 'subtitle_codec': item['subtitle_codec'],
'stream_bitrate': item['stream_bitrate'], 'stream_bitrate': item['stream_bitrate'],
'stream_video_resolution': item['stream_video_resolution'], 'stream_video_full_resolution': item['stream_video_full_resolution'],
'quality_profile': item['quality_profile'], 'quality_profile': item['quality_profile'],
'stream_container_decision': item['stream_container_decision'], 'stream_container_decision': item['stream_container_decision'],
'stream_container': item['stream_container'], 'stream_container': item['stream_container'],

View File

@@ -698,7 +698,7 @@ class Graphs(object):
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT UPPER(session_history_media_info.video_resolution) AS resolution, ' \ query = 'SELECT session_history_media_info.video_full_resolution AS resolution, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
'THEN 1 ELSE 0 END) AS dp_count, ' \ 'THEN 1 ELSE 0 END) AS dp_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
@@ -717,7 +717,7 @@ class Graphs(object):
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
query = 'SELECT UPPER(session_history_media_info.video_resolution) AS resolution,' \ query = 'SELECT session_history_media_info.video_full_resolution AS resolution,' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \ 'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
@@ -749,10 +749,6 @@ class Graphs(object):
series_3 = [] series_3 = []
for item in result: for item in result:
if item['resolution'] not in ('4k', 'unknown'):
item['resolution'] = item['resolution'].upper()
if item['resolution'].isdigit():
item['resolution'] += 'p'
categories.append(item['resolution']) categories.append(item['resolution'])
series_1.append(item['dp_count']) series_1.append(item['dp_count'])
series_2.append(item['ds_count']) series_2.append(item['ds_count'])
@@ -789,7 +785,7 @@ class Graphs(object):
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT ' \ query = 'SELECT ' \
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \ '(CASE WHEN session_history_media_info.stream_video_full_resolution IS NULL THEN ' \
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \ '(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
'(CASE ' \ '(CASE ' \
'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \ 'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
@@ -799,8 +795,8 @@ class Graphs(object):
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \ 'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \ 'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \ 'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
'ELSE "unknown" END) ELSE UPPER(session_history_media_info.video_resolution) END) ' \ 'ELSE "unknown" END) ELSE session_history_media_info.video_full_resolution END) ' \
'ELSE UPPER(session_history_media_info.stream_video_resolution) END) AS resolution, ' \ 'ELSE session_history_media_info.stream_video_full_resolution END) AS resolution, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
'THEN 1 ELSE 0 END) AS dp_count, ' \ 'THEN 1 ELSE 0 END) AS dp_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
@@ -820,7 +816,7 @@ class Graphs(object):
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
query = 'SELECT ' \ query = 'SELECT ' \
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \ '(CASE WHEN session_history_media_info.stream_video_full_resolution IS NULL THEN ' \
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \ '(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
'(CASE ' \ '(CASE ' \
'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \ 'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
@@ -830,8 +826,8 @@ class Graphs(object):
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \ 'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \ 'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \ 'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
'ELSE "unknown" END) ELSE UPPER(session_history_media_info.video_resolution) END) ' \ 'ELSE "unknown" END) ELSE session_history_media_info.video_full_resolution END) ' \
'ELSE UPPER(session_history_media_info.stream_video_resolution) END) AS resolution, ' \ 'ELSE session_history_media_info.stream_video_full_resolution END) AS resolution, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \ 'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
@@ -863,10 +859,6 @@ class Graphs(object):
series_3 = [] series_3 = []
for item in result: for item in result:
if item['resolution'] not in ('4k', 'unknown'):
item['resolution'] = item['resolution'].upper()
if item['resolution'].isdigit():
item['resolution'] += 'p'
categories.append(item['resolution']) categories.append(item['resolution'])
series_1.append(item['dp_count']) series_1.append(item['dp_count'])
series_2.append(item['ds_count']) series_2.append(item['ds_count'])

View File

@@ -1178,3 +1178,18 @@ def split_args(args=None):
return [arg.decode(plexpy.SYS_ENCODING, 'ignore') return [arg.decode(plexpy.SYS_ENCODING, 'ignore')
for arg in shlex.split(args.encode(plexpy.SYS_ENCODING, 'ignore'))] for arg in shlex.split(args.encode(plexpy.SYS_ENCODING, 'ignore'))]
return [] return []
def mask_config_passwords(config):
if isinstance(config, list):
for cfg in config:
if 'password' in cfg.get('name', '') and cfg.get('value', '') != '':
cfg['value'] = ' '
elif isinstance(config, dict):
for cfg, val in config.iteritems():
# Check for a password config keys and if the password is not blank
if 'password' in cfg and val != '':
# Set the password to blank so it is not exposed in the HTML form
config[cfg] = ' '
return config

View File

@@ -239,7 +239,7 @@ class Libraries(object):
def __init__(self): def __init__(self):
pass pass
def get_datatables_list(self, kwargs=None): def get_datatables_list(self, kwargs=None, grouping=None):
default_return = {'recordsFiltered': 0, default_return = {'recordsFiltered': 0,
'recordsTotal': 0, 'recordsTotal': 0,
'draw': 0, 'draw': 0,
@@ -250,9 +250,14 @@ class Libraries(object):
custom_where = [['library_sections.deleted_section', 0]] custom_where = [['library_sections.deleted_section', 0]]
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
if session.get_session_shared_libraries(): if session.get_session_shared_libraries():
custom_where.append(['library_sections.section_id', session.get_session_shared_libraries()]) custom_where.append(['library_sections.section_id', session.get_session_shared_libraries()])
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
columns = ['library_sections.section_id', columns = ['library_sections.section_id',
'library_sections.section_name', 'library_sections.section_name',
'library_sections.section_type', 'library_sections.section_type',
@@ -262,7 +267,7 @@ class Libraries(object):
'library_sections.thumb AS library_thumb', 'library_sections.thumb AS library_thumb',
'library_sections.custom_thumb_url AS custom_thumb', 'library_sections.custom_thumb_url AS custom_thumb',
'library_sections.art', 'library_sections.art',
'COUNT(session_history.id) AS plays', 'COUNT(DISTINCT %s) AS plays' % group_by,
'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \ 'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \ ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
session_history.paused_counter END) AS duration', session_history.paused_counter END) AS duration',
@@ -533,7 +538,7 @@ class Libraries(object):
# Search results # Search results
search_value = json_data['search']['value'].lower() search_value = json_data['search']['value'].lower()
if search_value: if search_value:
searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] + ['title']
for row in rows: for row in rows:
for k,v in row.iteritems(): for k,v in row.iteritems():
if k in searchable_columns and search_value in v.lower(): if k in searchable_columns and search_value in v.lower():

View File

@@ -61,13 +61,11 @@ def schedule_newsletter_job(newsletter_job_id, name='', func=None, remove_job=Fa
logger.info(u"Tautulli NewsletterHandler :: Removed scheduled newsletter: %s" % name) logger.info(u"Tautulli NewsletterHandler :: Removed scheduled newsletter: %s" % name)
else: else:
NEWSLETTER_SCHED.reschedule_job( NEWSLETTER_SCHED.reschedule_job(
newsletter_job_id, args=args, trigger=CronTrigger().from_crontab( newsletter_job_id, args=args, trigger=CronTrigger.from_crontab(cron))
cron, timezone=plexpy.SYS_TIMEZONE))
logger.info(u"Tautulli NewsletterHandler :: Re-scheduled newsletter: %s" % name) logger.info(u"Tautulli NewsletterHandler :: Re-scheduled newsletter: %s" % name)
elif not remove_job: elif not remove_job:
NEWSLETTER_SCHED.add_job( NEWSLETTER_SCHED.add_job(
func, args=args, id=newsletter_job_id, trigger=CronTrigger().from_crontab( func, args=args, id=newsletter_job_id, trigger=CronTrigger.from_crontab(cron))
cron, timezone=plexpy.SYS_TIMEZONE))
logger.info(u"Tautulli NewsletterHandler :: Scheduled newsletter: %s" % name) logger.info(u"Tautulli NewsletterHandler :: Scheduled newsletter: %s" % name)

View File

@@ -125,7 +125,7 @@ def delete_newsletter(newsletter_id=None):
return False return False
def get_newsletter_config(newsletter_id=None): def get_newsletter_config(newsletter_id=None, mask_passwords=False):
if str(newsletter_id).isdigit(): if str(newsletter_id).isdigit():
newsletter_id = int(newsletter_id) newsletter_id = int(newsletter_id)
else: else:
@@ -153,13 +153,16 @@ def get_newsletter_config(newsletter_id=None):
logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e) logger.error(u"Tautulli Newsletters :: Failed to get newsletter config options: %s." % e)
return return
if mask_passwords:
newsletter_agent.email_config = helpers.mask_config_passwords(newsletter_agent.email_config)
result['subject'] = newsletter_agent.subject result['subject'] = newsletter_agent.subject
result['body'] = newsletter_agent.body result['body'] = newsletter_agent.body
result['message'] = newsletter_agent.message result['message'] = newsletter_agent.message
result['config'] = newsletter_agent.config result['config'] = newsletter_agent.config
result['email_config'] = newsletter_agent.email_config result['email_config'] = newsletter_agent.email_config
result['config_options'] = newsletter_agent.return_config_options() result['config_options'] = newsletter_agent.return_config_options(mask_passwords=mask_passwords)
result['email_config_options'] = newsletter_agent.return_email_config_options() result['email_config_options'] = newsletter_agent.return_email_config_options(mask_passwords=mask_passwords)
return result return result
@@ -230,6 +233,13 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
email_config = {k[len(email_config_prefix):]: kwargs.pop(k) email_config = {k[len(email_config_prefix):]: kwargs.pop(k)
for k in kwargs.keys() if k.startswith(email_config_prefix)} for k in kwargs.keys() if k.startswith(email_config_prefix)}
for cfg, val in email_config.iteritems():
# Check for a password config keys and a blank password from the HTML form
if 'password' in cfg and val == ' ':
# Get the previous password so we don't overwrite it with a blank value
old_newsletter_config = get_newsletter_config(newsletter_id=newsletter_id)
email_config[cfg] = old_newsletter_config['email_config'][cfg]
subject = kwargs.pop('subject') subject = kwargs.pop('subject')
body = kwargs.pop('body') body = kwargs.pop('body')
message = kwargs.pop('message') message = kwargs.pop('message')
@@ -647,16 +657,21 @@ class Newsletter(object):
return filename return filename
def return_config_options(self): def return_config_options(self, mask_passwords=False):
return self._return_config_options() config_options = self._return_config_options()
def _return_config_options(self): # Mask password config options
config_options = [] if mask_passwords:
helpers.mask_config_passwords(config_options)
return config_options return config_options
def return_email_config_options(self): def _return_config_options(self):
config_options = EMAIL(self.email_config).return_config_options() config_options = []
return config_options
def return_email_config_options(self, mask_passwords=False):
config_options = EMAIL(self.email_config).return_config_options(mask_passwords=mask_passwords)
for c in config_options: for c in config_options:
c['name'] = 'newsletter_' + c['name'] c['name'] = 'newsletter_' + c['name']
return config_options return config_options
@@ -926,10 +941,8 @@ class RecentlyAdded(Newsletter):
return parameters return parameters
def return_config_options(self): def _return_config_options(self):
config_options = self._return_config_options() config_options = [
additional_config = [
{'label': 'Included Libraries', {'label': 'Included Libraries',
'value': self.config['incl_libraries'], 'value': self.config['incl_libraries'],
'description': 'Select the libraries to include in the newsletter.', 'description': 'Select the libraries to include in the newsletter.',
@@ -939,4 +952,4 @@ class RecentlyAdded(Newsletter):
} }
] ]
return additional_config + config_options return config_options

View File

@@ -784,8 +784,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'stream_video_bitrate': notify_params['stream_video_bitrate'], 'stream_video_bitrate': notify_params['stream_video_bitrate'],
'stream_video_bit_depth': notify_params['stream_video_bit_depth'], 'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
'stream_video_framerate': notify_params['stream_video_framerate'], 'stream_video_framerate': notify_params['stream_video_framerate'],
'stream_video_full_resolution': notify_params['stream_video_full_resolution'],
'stream_video_ref_frames': notify_params['stream_video_ref_frames'], 'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
'stream_video_resolution': notify_params['stream_video_resolution'], 'stream_video_resolution': notify_params['stream_video_resolution'],
'stream_video_scan_type': notify_params['stream_video_scan_type'],
'stream_video_height': notify_params['stream_video_height'], 'stream_video_height': notify_params['stream_video_height'],
'stream_video_width': notify_params['stream_video_width'], 'stream_video_width': notify_params['stream_video_width'],
'stream_video_language': notify_params['stream_video_language'], 'stream_video_language': notify_params['stream_video_language'],
@@ -890,8 +892,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'video_bitrate': notify_params['video_bitrate'], 'video_bitrate': notify_params['video_bitrate'],
'video_bit_depth': notify_params['video_bit_depth'], 'video_bit_depth': notify_params['video_bit_depth'],
'video_framerate': notify_params['video_framerate'], 'video_framerate': notify_params['video_framerate'],
'video_full_resolution': notify_params['video_full_resolution'],
'video_ref_frames': notify_params['video_ref_frames'], 'video_ref_frames': notify_params['video_ref_frames'],
'video_resolution': notify_params['video_resolution'], 'video_resolution': notify_params['video_resolution'],
'video_scan_type': notify_params['video_scan_type'],
'video_height': notify_params['height'], 'video_height': notify_params['height'],
'video_width': notify_params['width'], 'video_width': notify_params['width'],
'video_language': notify_params['video_language'], 'video_language': notify_params['video_language'],
@@ -1101,6 +1105,8 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
def strip_tag(data, agent_id=None): def strip_tag(data, agent_id=None):
# Substitute temporary tokens for < and > in parameter prefix and suffix
data = re.sub(r'{.+?}', lambda m: m.group().replace('<', '%temp_lt_token%').replace('>', '%temp_gt_token%'), data)
if agent_id == 7: if agent_id == 7:
# Allow tags b, i, u, a[href], font[color] for Pushover # Allow tags b, i, u, a[href], font[color] for Pushover
@@ -1109,11 +1115,11 @@ def strip_tag(data, agent_id=None):
'u': [], 'u': [],
'a': ['href'], 'a': ['href'],
'font': ['color']} 'font': ['color']}
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True) data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
elif agent_id in (10, 14, 20): elif agent_id in (10, 14, 20):
# Don't remove tags for Email, Slack, and Discord # Don't remove tags for Email, Slack, and Discord
return data pass
elif agent_id == 13: elif agent_id == 13:
# Allow tags b, i, code, pre, a[href] for Telegram # Allow tags b, i, code, pre, a[href] for Telegram
@@ -1122,11 +1128,14 @@ def strip_tag(data, agent_id=None):
'code': [], 'code': [],
'pre': [], 'pre': [],
'a': ['href']} 'a': ['href']}
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True) data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
else: else:
whitelist = {} whitelist = {}
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True) data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
# Resubstitute temporary tokens for < and > in parameter prefix and suffix
return data.replace('%temp_lt_token%', '<').replace('%temp_gt_token%', '>')
def format_group_index(group_keys): def format_group_index(group_keys):
@@ -1332,6 +1341,7 @@ def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
'tvmaze_json': json.dumps(tvmaze_json)} 'tvmaze_json': json.dumps(tvmaze_json)}
db.upsert(table_name='tvmaze_lookup', key_dict=keys, value_dict=tvmaze_info) db.upsert(table_name='tvmaze_lookup', key_dict=keys, value_dict=tvmaze_info)
tvmaze_info.update(keys)
tvmaze_info.pop('tvmaze_json') tvmaze_info.pop('tvmaze_json')
else: else:
@@ -1394,6 +1404,7 @@ def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info) db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info)
themoviedb_info.update(keys)
themoviedb_info.pop('themoviedb_json') themoviedb_info.pop('themoviedb_json')
else: else:
@@ -1447,6 +1458,8 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info) db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info)
themoviedb_info.update(keys)
else: else:
if err_msg: if err_msg:
logger.error(u"Tautulli NotificationHandler :: {}".format(err_msg)) logger.error(u"Tautulli NotificationHandler :: {}".format(err_msg))

View File

@@ -455,7 +455,7 @@ def delete_notifier(notifier_id=None):
return False return False
def get_notifier_config(notifier_id=None): def get_notifier_config(notifier_id=None, mask_passwords=False):
if str(notifier_id).isdigit(): if str(notifier_id).isdigit():
notifier_id = int(notifier_id) notifier_id = int(notifier_id)
else: else:
@@ -472,11 +472,13 @@ def get_notifier_config(notifier_id=None):
try: try:
config = json.loads(result.pop('notifier_config', '{}')) config = json.loads(result.pop('notifier_config', '{}'))
notifier_agent = get_agent_class(agent_id=result['agent_id'], config=config) notifier_agent = get_agent_class(agent_id=result['agent_id'], config=config)
notifier_config = notifier_agent.return_config_options()
except Exception as e: except Exception as e:
logger.error(u"Tautulli Notifiers :: Failed to get notifier config options: %s." % e) logger.error(u"Tautulli Notifiers :: Failed to get notifier config options: %s." % e)
return return
if mask_passwords:
notifier_agent.config = helpers.mask_config_passwords(notifier_agent.config)
notify_actions = get_notify_actions(return_dict=True) notify_actions = get_notify_actions(return_dict=True)
notifier_actions = {} notifier_actions = {}
@@ -503,8 +505,8 @@ def get_notifier_config(notifier_id=None):
if not result['custom_conditions_logic']: if not result['custom_conditions_logic']:
result['custom_conditions_logic'] = '' result['custom_conditions_logic'] = ''
result['config'] = config result['config'] = notifier_agent.config
result['config_options'] = notifier_config result['config_options'] = notifier_agent.return_config_options(mask_passwords=mask_passwords)
result['actions'] = notifier_actions result['actions'] = notifier_actions
result['notify_text'] = notifier_text result['notify_text'] = notifier_text
@@ -587,6 +589,13 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
notifier_config = {k[len(config_prefix):]: kwargs.pop(k) notifier_config = {k[len(config_prefix):]: kwargs.pop(k)
for k in kwargs.keys() if k.startswith(config_prefix)} for k in kwargs.keys() if k.startswith(config_prefix)}
for cfg, val in notifier_config.iteritems():
# Check for a password config keys and a blank password from the HTML form
if 'password' in cfg and val == ' ':
# Get the previous password so we don't overwrite it with a blank value
old_notifier_config = get_notifier_config(notifier_id=notifier_id)
notifier_config[cfg] = old_notifier_config['config'][cfg]
agent_class = get_agent_class(agent_id=agent['id'], config=notifier_config) agent_class = get_agent_class(agent_id=agent['id'], config=notifier_config)
keys = {'id': notifier_id} keys = {'id': notifier_id}
@@ -835,7 +844,16 @@ class Notifier(object):
return False return False
def return_config_options(self): def return_config_options(self, mask_passwords=False):
config_options = self._return_config_options()
# Mask password config options
if mask_passwords:
helpers.mask_config_passwords(config_options)
return config_options
def _return_config_options(self):
config_options = [] config_options = []
return config_options return config_options
@@ -942,7 +960,7 @@ class ANDROIDAPP(Notifier):
return devices return devices
def return_config_options(self): def _return_config_options(self):
config_option = [] config_option = []
if not CRYPTODOME: if not CRYPTODOME:
@@ -1058,7 +1076,7 @@ class BOXCAR(Notifier):
return sounds return sounds
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Boxcar Access Token', config_option = [{'label': 'Boxcar Access Token',
'value': self.config['token'], 'value': self.config['token'],
'name': 'boxcar_token', 'name': 'boxcar_token',
@@ -1089,7 +1107,7 @@ class BROWSER(Notifier):
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME)) logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
return True return True
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Allow Notifications', config_option = [{'label': 'Allow Notifications',
'value': 'Allow Notifications', 'value': 'Allow Notifications',
'name': 'browser_allow_browser', 'name': 'browser_allow_browser',
@@ -1202,7 +1220,7 @@ class DISCORD(Notifier):
return self.make_request(self.config['hook'], params=params, headers=headers, json=data) return self.make_request(self.config['hook'], params=params, headers=headers, json=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Discord Webhook URL', config_option = [{'label': 'Discord Webhook URL',
'value': self.config['hook'], 'value': self.config['hook'],
'name': 'discord_hook', 'name': 'discord_hook',
@@ -1389,7 +1407,7 @@ class EMAIL(Notifier):
return user_emails_to, user_emails_cc, user_emails_bcc return user_emails_to, user_emails_cc, user_emails_bcc
def return_config_options(self): def _return_config_options(self):
user_emails_to, user_emails_cc, user_emails_bcc = self.get_user_emails() user_emails_to, user_emails_cc, user_emails_bcc = self.get_user_emails()
config_option = [{'label': 'From Name', config_option = [{'label': 'From Name',
@@ -1569,7 +1587,7 @@ class FACEBOOK(Notifier):
return self._post_facebook(**data) return self._post_facebook(**data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'OAuth Redirect URI', config_option = [{'label': 'OAuth Redirect URI',
'value': self.config['redirect_uri'], 'value': self.config['redirect_uri'],
'name': 'facebook_redirect_uri', 'name': 'facebook_redirect_uri',
@@ -1699,7 +1717,7 @@ class GROUPME(Notifier):
return self.make_request('https://api.groupme.com/v3/bots/post', json=data) return self.make_request('https://api.groupme.com/v3/bots/post', json=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'GroupMe Access Token', config_option = [{'label': 'GroupMe Access Token',
'value': self.config['access_token'], 'value': self.config['access_token'],
'name': 'groupme_access_token', 'name': 'groupme_access_token',
@@ -1796,7 +1814,7 @@ class GROWL(Notifier):
logger.error(u"Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME)) logger.error(u"Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME))
return False return False
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Growl Host', config_option = [{'label': 'Growl Host',
'value': self.config['host'], 'value': self.config['host'],
'name': 'growl_host', 'name': 'growl_host',
@@ -1901,7 +1919,7 @@ class HIPCHAT(Notifier):
return self.make_request(self.config['hook'], headers=headers, json=data) return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Hipchat Custom Integrations URL', config_option = [{'label': 'Hipchat Custom Integrations URL',
'value': self.config['hook'], 'value': self.config['hook'],
'name': 'hipchat_hook', 'name': 'hipchat_hook',
@@ -2012,7 +2030,7 @@ class IFTTT(Notifier):
return self.make_request('https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, self.config['key']), return self.make_request('https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, self.config['key']),
headers=headers, json=data) headers=headers, json=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'IFTTT Webhook Key', config_option = [{'label': 'IFTTT Webhook Key',
'value': self.config['key'], 'value': self.config['key'],
'name': 'ifttt_key', 'name': 'ifttt_key',
@@ -2131,7 +2149,7 @@ class JOIN(Notifier):
return devices return devices
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Join API Key', config_option = [{'label': 'Join API Key',
'value': self.config['api_key'], 'value': self.config['api_key'],
'name': 'join_api_key', 'name': 'join_api_key',
@@ -2233,7 +2251,7 @@ class MQTT(Notifier):
return True return True
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Broker', config_option = [{'label': 'Broker',
'value': self.config['broker'], 'value': self.config['broker'],
'name': 'mqtt_broker', 'name': 'mqtt_broker',
@@ -2335,7 +2353,7 @@ class NMA(Notifier):
logger.error(u"Tautulli Notifiers :: {name} notification failed.".format(name=self.NAME)) logger.error(u"Tautulli Notifiers :: {name} notification failed.".format(name=self.NAME))
return False return False
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'NotifyMyAndroid API Key', config_option = [{'label': 'NotifyMyAndroid API Key',
'value': self.config['api_key'], 'value': self.config['api_key'],
'name': 'nma_api_key', 'name': 'nma_api_key',
@@ -2437,7 +2455,7 @@ class OSX(Notifier):
logger.error(u"Tautulli Notifiers :: {name} failed: {e}".format(name=self.NAME, e=e)) logger.error(u"Tautulli Notifiers :: {name} failed: {e}".format(name=self.NAME, e=e))
return False return False
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Register Notify App', config_option = [{'label': 'Register Notify App',
'value': self.config['notify_app'], 'value': self.config['notify_app'],
'name': 'osx_notify_app', 'name': 'osx_notify_app',
@@ -2530,7 +2548,7 @@ class PLEX(Notifier):
return True return True
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Plex Home Theater Host Address', config_option = [{'label': 'Plex Home Theater Host Address',
'value': self.config['hosts'], 'value': self.config['hosts'],
'name': 'plex_hosts', 'name': 'plex_hosts',
@@ -2586,7 +2604,7 @@ class PROWL(Notifier):
return self.make_request('https://api.prowlapp.com/publicapi/add', headers=headers, data=data) return self.make_request('https://api.prowlapp.com/publicapi/add', headers=headers, data=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Prowl API Key', config_option = [{'label': 'Prowl API Key',
'value': self.config['key'], 'value': self.config['key'],
'name': 'prowl_key', 'name': 'prowl_key',
@@ -2622,7 +2640,7 @@ class PUSHALOT(Notifier):
return self.make_request('https://pushalot.com/api/sendmessage', headers=headers, data=data) return self.make_request('https://pushalot.com/api/sendmessage', headers=headers, data=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Pushalot API Key', config_option = [{'label': 'Pushalot API Key',
'value': self.config['api_key'], 'value': self.config['api_key'],
'name': 'pushalot_api_key', 'name': 'pushalot_api_key',
@@ -2722,7 +2740,7 @@ class PUSHBULLET(Notifier):
return devices return devices
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Pushbullet Access Token', config_option = [{'label': 'Pushbullet Access Token',
'value': self.config['api_key'], 'value': self.config['api_key'],
'name': 'pushbullet_api_key', 'name': 'pushbullet_api_key',
@@ -2888,7 +2906,7 @@ class PUSHOVER(Notifier):
# else: # else:
# return {'': ''} # return {'': ''}
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Pushover API Token', config_option = [{'label': 'Pushover API Token',
'value': self.config['api_token'], 'value': self.config['api_token'],
'name': 'pushover_api_token', 'name': 'pushover_api_token',
@@ -3163,7 +3181,7 @@ class SCRIPTS(Notifier):
return True return True
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Supported File Types', config_option = [{'label': 'Supported File Types',
'description': '<span class="inline-pre">' + \ 'description': '<span class="inline-pre">' + \
', '.join(self.script_exts.keys()) + '</span>', ', '.join(self.script_exts.keys()) + '</span>',
@@ -3286,7 +3304,7 @@ class SLACK(Notifier):
return self.make_request(self.config['hook'], headers=headers, json=data) return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Slack Webhook URL', config_option = [{'label': 'Slack Webhook URL',
'value': self.config['hook'], 'value': self.config['hook'],
'name': 'slack_hook', 'name': 'slack_hook',
@@ -3439,7 +3457,7 @@ class TELEGRAM(Notifier):
return self.make_request('https://api.telegram.org/bot{}/sendMessage'.format(self.config['bot_token']), return self.make_request('https://api.telegram.org/bot{}/sendMessage'.format(self.config['bot_token']),
headers=headers, data=data) headers=headers, data=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Telegram Bot Token', config_option = [{'label': 'Telegram Bot Token',
'value': self.config['bot_token'], 'value': self.config['bot_token'],
'name': 'telegram_bot_token', 'name': 'telegram_bot_token',
@@ -3537,7 +3555,7 @@ class TWITTER(Notifier):
else: else:
return self._send_tweet(body, attachment=poster_url) return self._send_tweet(body, attachment=poster_url)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Twitter Consumer Key', config_option = [{'label': 'Twitter Consumer Key',
'value': self.config['consumer_key'], 'value': self.config['consumer_key'],
'name': 'twitter_consumer_key', 'name': 'twitter_consumer_key',
@@ -3606,7 +3624,7 @@ class WEBHOOK(Notifier):
return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, json=webhook_data) return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, json=webhook_data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Webhook URL', config_option = [{'label': 'Webhook URL',
'value': self.config['hook'], 'value': self.config['hook'],
'name': 'webhook_hook', 'name': 'webhook_hook',
@@ -3703,7 +3721,7 @@ class XBMC(Notifier):
return True return True
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Kodi Host Address', config_option = [{'label': 'Kodi Host Address',
'value': self.config['hosts'], 'value': self.config['hosts'],
'name': 'xbmc_hosts', 'name': 'xbmc_hosts',
@@ -3796,7 +3814,7 @@ class ZAPIER(Notifier):
return self.make_request(self.config['hook'], headers=headers, json=data) return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Zapier Webhook URL', config_option = [{'label': 'Zapier Webhook URL',
'value': self.config['hook'], 'value': self.config['hook'],
'name': 'zapier_hook', 'name': 'zapier_hook',

View File

@@ -710,6 +710,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -760,6 +762,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -807,6 +811,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': show_details['directors'], 'directors': show_details['directors'],
'writers': show_details['writers'], 'writers': show_details['writers'],
'actors': show_details['actors'], 'actors': show_details['actors'],
@@ -871,6 +877,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': show_details['actors'], 'actors': show_details['actors'],
@@ -917,6 +925,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -964,6 +974,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -1014,6 +1026,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -1060,6 +1074,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -1107,6 +1123,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -1156,6 +1174,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'child_count': helpers.get_xml_attr(metadata_main, 'childCount'), 'child_count': helpers.get_xml_attr(metadata_main, 'childCount'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
@@ -1202,6 +1222,8 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'parent_guid': helpers.get_xml_attr(metadata_main, 'parentGuid'),
'grandparent_guid': helpers.get_xml_attr(metadata_main, 'grandparentGuid'),
'directors': directors, 'directors': directors,
'writers': writers, 'writers': writers,
'actors': actors, 'actors': actors,
@@ -1220,6 +1242,7 @@ class PmsConnect(object):
medias = [] medias = []
media_items = metadata_main.getElementsByTagName('Media') media_items = metadata_main.getElementsByTagName('Media')
for media in media_items: for media in media_items:
video_full_resolution_scan_type = None
parts = [] parts = []
part_items = media.getElementsByTagName('Part') part_items = media.getElementsByTagName('Part')
@@ -1229,6 +1252,10 @@ class PmsConnect(object):
stream_items = part.getElementsByTagName('Stream') stream_items = part.getElementsByTagName('Stream')
for stream in stream_items: for stream in stream_items:
if helpers.get_xml_attr(stream, 'streamType') == '1': if helpers.get_xml_attr(stream, 'streamType') == '1':
video_scan_type = helpers.get_xml_attr(stream, 'scanType')
if video_full_resolution_scan_type is None:
video_full_resolution_scan_type = video_scan_type
streams.append({'id': helpers.get_xml_attr(stream, 'id'), streams.append({'id': helpers.get_xml_attr(stream, 'id'),
'type': helpers.get_xml_attr(stream, 'streamType'), 'type': helpers.get_xml_attr(stream, 'streamType'),
'video_codec': helpers.get_xml_attr(stream, 'codec'), 'video_codec': helpers.get_xml_attr(stream, 'codec'),
@@ -1242,6 +1269,7 @@ class PmsConnect(object):
'video_language': helpers.get_xml_attr(stream, 'language'), 'video_language': helpers.get_xml_attr(stream, 'language'),
'video_language_code': helpers.get_xml_attr(stream, 'languageCode'), 'video_language_code': helpers.get_xml_attr(stream, 'languageCode'),
'video_profile': helpers.get_xml_attr(stream, 'profile'), 'video_profile': helpers.get_xml_attr(stream, 'profile'),
'video_scan_type': helpers.get_xml_attr(stream, 'scanType'),
'selected': int(helpers.get_xml_attr(stream, 'selected') == '1') 'selected': int(helpers.get_xml_attr(stream, 'selected') == '1')
}) })
@@ -1281,6 +1309,13 @@ class PmsConnect(object):
'selected': int(helpers.get_xml_attr(part, 'selected') == '1') 'selected': int(helpers.get_xml_attr(part, 'selected') == '1')
}) })
video_resolution = helpers.get_xml_attr(media, 'videoResolution').lower()
video_full_resolution = ''
if video_full_resolution_scan_type is not None:
video_full_resolution = common.VIDEO_RESOLUTION_OVERRIDES.get(
video_resolution, video_resolution + (video_full_resolution_scan_type[:1] or 'p')
)
audio_channels = helpers.get_xml_attr(media, 'audioChannels') audio_channels = helpers.get_xml_attr(media, 'audioChannels')
medias.append({'id': helpers.get_xml_attr(media, 'id'), medias.append({'id': helpers.get_xml_attr(media, 'id'),
@@ -1290,7 +1325,8 @@ class PmsConnect(object):
'width': helpers.get_xml_attr(media, 'width'), 'width': helpers.get_xml_attr(media, 'width'),
'aspect_ratio': helpers.get_xml_attr(media, 'aspectRatio'), 'aspect_ratio': helpers.get_xml_attr(media, 'aspectRatio'),
'video_codec': helpers.get_xml_attr(media, 'videoCodec'), 'video_codec': helpers.get_xml_attr(media, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(media, 'videoResolution'), 'video_resolution': video_resolution,
'video_full_resolution': video_full_resolution,
'video_framerate': helpers.get_xml_attr(media, 'videoFrameRate'), 'video_framerate': helpers.get_xml_attr(media, 'videoFrameRate'),
'video_profile': helpers.get_xml_attr(media, 'videoProfile'), 'video_profile': helpers.get_xml_attr(media, 'videoProfile'),
'audio_codec': helpers.get_xml_attr(media, 'audioCodec'), 'audio_codec': helpers.get_xml_attr(media, 'audioCodec'),
@@ -1301,6 +1337,8 @@ class PmsConnect(object):
'parts': parts 'parts': parts
}) })
video_full_resolution = helpers.get_xml_attr(media, 'videoResolution').lower()
metadata['media_info'] = medias metadata['media_info'] = medias
if metadata: if metadata:
@@ -1644,12 +1682,15 @@ class PmsConnect(object):
video_stream_info = audio_stream_info = subtitle_stream_info = None video_stream_info = audio_stream_info = subtitle_stream_info = None
for stream in stream_media_parts_info.getElementsByTagName('Stream'): for stream in stream_media_parts_info.getElementsByTagName('Stream'):
if helpers.get_xml_attr(stream, 'streamType') == '1': if helpers.get_xml_attr(stream, 'streamType') == '1':
if video_stream_info is None or helpers.get_xml_attr(stream, 'selected') == '1':
video_stream_info = stream video_stream_info = stream
elif helpers.get_xml_attr(stream, 'streamType') == '2' and helpers.get_xml_attr(stream, 'selected') == '1': elif helpers.get_xml_attr(stream, 'streamType') == '2':
if audio_stream_info is None or helpers.get_xml_attr(stream, 'selected') == '1':
audio_stream_info = stream audio_stream_info = stream
elif helpers.get_xml_attr(stream, 'streamType') == '3' and helpers.get_xml_attr(stream, 'selected') == '1': elif helpers.get_xml_attr(stream, 'streamType') == '3':
if subtitle_stream_info is None or helpers.get_xml_attr(stream, 'selected') == '1':
subtitle_stream_info = stream subtitle_stream_info = stream
video_id = audio_id = subtitle_id = None video_id = audio_id = subtitle_id = None
@@ -1661,6 +1702,7 @@ class PmsConnect(object):
'stream_video_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'), 'stream_video_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'),
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'), 'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
'stream_video_language_code': helpers.get_xml_attr(video_stream_info, 'languageCode'), 'stream_video_language_code': helpers.get_xml_attr(video_stream_info, 'languageCode'),
'stream_video_scan_type': helpers.get_xml_attr(video_stream_info, 'scanType'),
'stream_video_decision': helpers.get_xml_attr(video_stream_info, 'decision') or 'direct play' 'stream_video_decision': helpers.get_xml_attr(video_stream_info, 'decision') or 'direct play'
} }
else: else:
@@ -1670,6 +1712,7 @@ class PmsConnect(object):
'stream_video_ref_frames': '', 'stream_video_ref_frames': '',
'stream_video_language': '', 'stream_video_language': '',
'stream_video_language_code': '', 'stream_video_language_code': '',
'stream_video_scan_type': '',
'stream_video_decision': '' 'stream_video_decision': ''
} }
@@ -1730,7 +1773,7 @@ class PmsConnect(object):
if helpers.cast_to_int(stream_video_width) >= 3840: if helpers.cast_to_int(stream_video_width) >= 3840:
stream_video_resolution = '4k' stream_video_resolution = '4k'
else: else:
stream_video_resolution = helpers.get_xml_attr(stream_media_info, 'videoResolution').rstrip('p') stream_video_resolution = helpers.get_xml_attr(stream_media_info, 'videoResolution').rstrip('p').lower()
stream_audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels') stream_audio_channels = helpers.get_xml_attr(stream_media_info, 'audioChannels')
@@ -1810,7 +1853,7 @@ class PmsConnect(object):
'height': helpers.get_xml_attr(stream_media_info, 'height'), 'height': helpers.get_xml_attr(stream_media_info, 'height'),
'width': helpers.get_xml_attr(stream_media_info, 'width'), 'width': helpers.get_xml_attr(stream_media_info, 'width'),
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'), 'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution'), 'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(),
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'), 'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
'audio_channels': audio_channels, 'audio_channels': audio_channels,
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels), 'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
@@ -1849,6 +1892,7 @@ class PmsConnect(object):
'video_width': '', 'video_width': '',
'video_language': '', 'video_language': '',
'video_language_code': '', 'video_language_code': '',
'video_scan_type': '',
'video_profile': '' 'video_profile': ''
} }
source_audio_details = {'id': '', source_audio_details = {'id': '',
@@ -1919,6 +1963,12 @@ class PmsConnect(object):
if transcode_details['transcode_video_codec'] == '*': if transcode_details['transcode_video_codec'] == '*':
transcode_details['transcode_video_codec'] = source_video_details['video_codec'] transcode_details['transcode_video_codec'] = source_video_details['video_codec']
if media_type in ('movie', 'episode', 'clip'):
# Set the full resolution by combining stream_video_resolution and stream_video_scan_type
stream_details['stream_video_full_resolution'] = common.VIDEO_RESOLUTION_OVERRIDES.get(
stream_details['stream_video_resolution'],
stream_details['stream_video_resolution'] + (video_details['stream_video_scan_type'][:1] or 'p'))
# Get the quality profile # Get the quality profile
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details: if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
if sync_id: if sync_id:
@@ -1945,8 +1995,7 @@ class PmsConnect(object):
if stream_details['optimized_version']: if stream_details['optimized_version']:
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate')) source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1), optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'], source_media_details['video_full_resolution'])
source_media_details['video_resolution']))
else: else:
optimized_version_profile = '' optimized_version_profile = ''
@@ -2467,7 +2516,7 @@ class PmsConnect(object):
media_info = {'container': helpers.get_xml_attr(media, 'container'), media_info = {'container': helpers.get_xml_attr(media, 'container'),
'bitrate': helpers.get_xml_attr(media, 'bitrate'), 'bitrate': helpers.get_xml_attr(media, 'bitrate'),
'video_codec': helpers.get_xml_attr(media, 'videoCodec'), 'video_codec': helpers.get_xml_attr(media, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(media, 'videoResolution'), 'video_resolution': helpers.get_xml_attr(media, 'videoResolution').lower(),
'video_framerate': helpers.get_xml_attr(media, 'videoFrameRate'), 'video_framerate': helpers.get_xml_attr(media, 'videoFrameRate'),
'audio_codec': helpers.get_xml_attr(media, 'audioCodec'), 'audio_codec': helpers.get_xml_attr(media, 'audioCodec'),
'audio_channels': helpers.get_xml_attr(media, 'audioChannels'), 'audio_channels': helpers.get_xml_attr(media, 'audioChannels'),

View File

@@ -70,7 +70,7 @@ class Users(object):
def __init__(self): def __init__(self):
pass pass
def get_datatables_list(self, kwargs=None): def get_datatables_list(self, kwargs=None, grouping=None):
default_return = {'recordsFiltered': 0, default_return = {'recordsFiltered': 0,
'recordsTotal': 0, 'recordsTotal': 0,
'draw': 0, 'draw': 0,
@@ -81,18 +81,23 @@ class Users(object):
custom_where = [['users.deleted_user', 0]] custom_where = [['users.deleted_user', 0]]
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
if session.get_session_user_id(): if session.get_session_user_id():
custom_where.append(['users.user_id', session.get_session_user_id()]) custom_where.append(['users.user_id', session.get_session_user_id()])
if kwargs.get('user_id'): if kwargs.get('user_id'):
custom_where.append(['users.user_id', kwargs.get('user_id')]) custom_where.append(['users.user_id', kwargs.get('user_id')])
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
columns = ['users.user_id', columns = ['users.user_id',
'(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" \ '(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" \
THEN users.username ELSE users.friendly_name END) AS friendly_name', THEN users.username ELSE users.friendly_name END) AS friendly_name',
'users.thumb AS user_thumb', 'users.thumb AS user_thumb',
'users.custom_avatar_url AS custom_thumb', 'users.custom_avatar_url AS custom_thumb',
'COUNT(session_history.id) AS plays', 'COUNT(DISTINCT %s) AS plays' % group_by,
'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \ 'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \ ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
session_history.paused_counter END) AS duration', session_history.paused_counter END) AS duration',

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.1.31-beta" PLEXPY_RELEASE_VERSION = "v2.1.36-beta"

View File

@@ -353,7 +353,7 @@ class WebInterface(object):
@requireAuth() @requireAuth()
@sanitize_out() @sanitize_out()
@addtoapi("get_libraries_table") @addtoapi("get_libraries_table")
def get_library_list(self, **kwargs): def get_library_list(self, grouping=None, **kwargs):
""" Get the data on the Tautulli libraries table. """ Get the data on the Tautulli libraries table.
``` ```
@@ -361,6 +361,7 @@ class WebInterface(object):
None None
Optional parameters: Optional parameters:
grouping (int): 0 or 1
order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count", order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count",
"child_count", "last_accessed", "last_played", "plays", "duration" "child_count", "last_accessed", "last_played", "plays", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
@@ -423,7 +424,7 @@ class WebInterface(object):
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "section_name") kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "section_name")
library_data = libraries.Libraries() library_data = libraries.Libraries()
library_list = library_data.get_datatables_list(kwargs=kwargs) library_list = library_data.get_datatables_list(kwargs=kwargs, grouping=grouping)
return library_list return library_list
@@ -1016,7 +1017,7 @@ class WebInterface(object):
@requireAuth() @requireAuth()
@sanitize_out() @sanitize_out()
@addtoapi("get_users_table") @addtoapi("get_users_table")
def get_user_list(self, **kwargs): def get_user_list(self, grouping=None, **kwargs):
""" Get the data on Tautulli users table. """ Get the data on Tautulli users table.
``` ```
@@ -1024,6 +1025,7 @@ class WebInterface(object):
None None
Optional parameters: Optional parameters:
grouping (int): 0 or 1
order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform", order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform",
"player", "last_played", "plays", "duration" "player", "last_played", "plays", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
@@ -1082,7 +1084,7 @@ class WebInterface(object):
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "friendly_name") kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "friendly_name")
user_data = users.Users() user_data = users.Users()
user_list = user_data.get_datatables_list(kwargs=kwargs) user_list = user_data.get_datatables_list(kwargs=kwargs, grouping=grouping)
return user_list return user_list
@@ -1628,8 +1630,9 @@ class WebInterface(object):
"parent_title": "", "parent_title": "",
"paused_counter": 0, "paused_counter": 0,
"percent_complete": 84, "percent_complete": 84,
"platform": "Chrome", "platform": "Windows",
"player": "Plex Web (Chrome)", "product": "Plex for Windows",
"player": "Castle-PC",
"rating_key": 4348, "rating_key": 4348,
"reference_id": 1123, "reference_id": 1123,
"session_key": null, "session_key": null,
@@ -1658,6 +1661,7 @@ class WebInterface(object):
("friendly_name", True, True), ("friendly_name", True, True),
("ip_address", True, True), ("ip_address", True, True),
("platform", True, True), ("platform", True, True),
("product", True, True),
("player", True, True), ("player", True, True),
("full_title", True, True), ("full_title", True, True),
("started", True, False), ("started", True, False),
@@ -2857,8 +2861,15 @@ class WebInterface(object):
kwargs['http_password'] = make_hash(kwargs['http_password']) kwargs['http_password'] = make_hash(kwargs['http_password'])
kwargs['http_hashed_password'] = 1 kwargs['http_hashed_password'] = 1
# Flag to refresh JWT uuid to log out clients
kwargs['jwt_update_secret'] = True
elif not kwargs.get('http_hash_password'): elif not kwargs.get('http_hash_password'):
kwargs['http_hashed_password'] = 0 kwargs['http_hashed_password'] = 0
# Flag to refresh JWT uuid to log out clients
kwargs['jwt_update_secret'] = True
else: else:
kwargs['http_hashed_password'] = 0 kwargs['http_hashed_password'] = 0
@@ -3168,13 +3179,13 @@ class WebInterface(object):
} }
``` ```
""" """
result = notifiers.get_notifier_config(notifier_id=notifier_id) result = notifiers.get_notifier_config(notifier_id=notifier_id, mask_passwords=True)
return result return result
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def get_notifier_config_modal(self, notifier_id=None, **kwargs): def get_notifier_config_modal(self, notifier_id=None, **kwargs):
result = notifiers.get_notifier_config(notifier_id=notifier_id) result = notifiers.get_notifier_config(notifier_id=notifier_id, mask_passwords=True)
parameters = [ parameters = [
{'name': param['name'], 'type': param['type'], 'value': param['value']} {'name': param['name'], 'type': param['type'], 'value': param['value']}
@@ -3657,10 +3668,10 @@ class WebInterface(object):
identifier = server['clientIdentifier'] identifier = server['clientIdentifier']
break break
# Fallback to checking /identity endpoint is server is unpublished # Fallback to checking /identity endpoint if the server is unpublished
# Cannot set SSL settings on the PMS if unpublished so 'http' is okay # Cannot set SSL settings on the PMS if unpublished so 'http' is okay
if not identifier: if not identifier:
scheme = 'https' if ssl else 'http' scheme = 'https' if helpers.cast_to_int(ssl) else 'http'
url = '{scheme}://{hostname}:{port}'.format(scheme=scheme, hostname=hostname, port=port) url = '{scheme}://{hostname}:{port}'.format(scheme=scheme, hostname=hostname, port=port)
uri = '/identity' uri = '/identity'
@@ -4548,6 +4559,7 @@ class WebInterface(object):
"Drama", "Drama",
"Fantasy" "Fantasy"
], ],
"grandparent_guid": "com.plexapp.agents.thetvdb://121361?lang=en",
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
@@ -4588,6 +4600,7 @@ class WebInterface(object):
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_scan_type": "progressive",
"video_width": "1920", "video_width": "1920",
"selected": 0 "selected": 0
}, },
@@ -4622,6 +4635,7 @@ class WebInterface(object):
], ],
"video_codec": "h264", "video_codec": "h264",
"video_framerate": "24p", "video_framerate": "24p",
"video_full_resolution": "1080p",
"video_profile": "high", "video_profile": "high",
"video_resolution": "1080", "video_resolution": "1080",
"width": "1920" "width": "1920"
@@ -4630,6 +4644,7 @@ class WebInterface(object):
"media_type": "episode", "media_type": "episode",
"original_title": "", "original_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -4940,6 +4955,7 @@ class WebInterface(object):
"Drama", "Drama",
"Fantasy" "Fantasy"
], ],
"grandparent_guid": "com.plexapp.agents.thetvdb://121361?lang=en",
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1503306930", "grandparent_thumb": "/library/metadata/1219/thumb/1503306930",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
@@ -4967,6 +4983,7 @@ class WebInterface(object):
"optimized_version_title": "", "optimized_version_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"original_title": "", "original_title": "",
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210", "parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -5031,7 +5048,9 @@ class WebInterface(object):
"stream_video_language": "", "stream_video_language": "",
"stream_video_language_code": "", "stream_video_language_code": "",
"stream_video_ref_frames": "4", "stream_video_ref_frames": "4",
"stream_video_full_resolution": "1080p",
"stream_video_resolution": "1080", "stream_video_resolution": "1080",
"stream_video_scan_type": "progressive",
"stream_video_width": "1920", "stream_video_width": "1920",
"studio": "HBO", "studio": "HBO",
"subtitle_codec": "", "subtitle_codec": "",
@@ -5084,12 +5103,14 @@ class WebInterface(object):
"video_decision": "direct play", "video_decision": "direct play",
"video_frame_rate": "23.976", "video_frame_rate": "23.976",
"video_framerate": "24p", "video_framerate": "24p",
"video_full_resolution": "1080p",
"video_height": "1078", "video_height": "1078",
"video_language": "", "video_language": "",
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_resolution": "1080", "video_resolution": "1080",
"video_scan_type": "progressive",
"video_width": "1920", "video_width": "1920",
"view_offset": "1000", "view_offset": "1000",
"width": "1920", "width": "1920",
@@ -5667,13 +5688,13 @@ class WebInterface(object):
} }
``` ```
""" """
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id) result = newsletters.get_newsletter_config(newsletter_id=newsletter_id, mask_passwords=True)
return result return result
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def get_newsletter_config_modal(self, newsletter_id=None, **kwargs): def get_newsletter_config_modal(self, newsletter_id=None, **kwargs):
result = newsletters.get_newsletter_config(newsletter_id=newsletter_id) result = newsletters.get_newsletter_config(newsletter_id=newsletter_id, mask_passwords=True)
return serve_template(templatename="newsletter_config.html", newsletter=result) return serve_template(templatename="newsletter_config.html", newsletter=result)
@cherrypy.expose @cherrypy.expose