This commit is contained in:
Tim 2015-10-12 20:22:39 +02:00
commit 5fe47d797f
32 changed files with 1476 additions and 372 deletions

57
API.md
View file

@ -4,24 +4,77 @@ The API is still pretty new and needs some serious cleaning up on the backend bu
## General structure ## General structure
The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command` The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command`
Data response in JSON formatted. Response example
```
{
"response": {
"data": [
{
"loglevel": "INFO",
"msg": "Signal 2 caught, saving and exiting...",
"thread": "MainThread",
"time": "22-sep-2015 01:42:56 "
}
],
"message": null,
"result": "success"
}
}
```
General parameters:
out_type: 'xml',
callback: 'pong',
'debug': 1
## API methods ## API methods
### getLogs ### getLogs
Not working yet Possible params: sort='', search='', order='desc', regex='', start=0, end=0
Returns the plexpy log
### getApikey
Possible params: username='', password='' (required if auth is enabled)
Returns the apikey
### getSettings
No params
Returns the config file
### getVersion ### getVersion
No params
Returns some version information: git_path, install_type, current_version, installed_version, commits_behind Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
### getHistory
possible params: user=None, user_id=None, ,rating_key='', parent_rating_key='', grandparent_rating_key='', start_date=''
Returns
### getMetadata
Required params: rating_key
Returns metadata about a file
### getSync
Possible params: machine_id=None, user_id=None,
Returns
### getUserips
Possible params: user_id=None, user=None
### getPlayby
Possible params: time_range=30, y_axis='plays', playtype='total_plays_per_month'
### checkGithub ### checkGithub
Updates the version information above and returns getVersion data Updates the version information above and returns getVersion data
### shutdown ### shutdown
No params
Shut down plexpy Shut down plexpy
### restart ### restart
No params
Restart plexpy Restart plexpy
### update ### update
No params
Update plexpy - you may want to check the install type in get version and not allow this if type==exe Update plexpy - you may want to check the install type in get version and not allow this if type==exe

View file

@ -1,5 +1,19 @@
# Changelog # Changelog
## v1.2.2 (2015-10-12)
* Add server discovery on first run.
* Add column to tables for Platform.
* Add link to top level breadcrumbs on info pages.
* Add ability to change notification sounds for Pushover and Boxcar.
* Show watched percentage tooltip on progress column in history tables.
* More logging in event an http request fails.
* Code cleanups and other fixes.
* Fix ordering on sync table.
* Fix bug on home stats cards.
* Fix bug on activity pane where music details were not shown.
## v1.2.1 (2015-09-29) ## v1.2.1 (2015-09-29)
* Fix for possible issue when paused_counter is null. * Fix for possible issue when paused_counter is null.

View file

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify

View file

@ -961,14 +961,24 @@ a:hover .dashboard-recent-media-cover {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.summary-navbar-list span { .summary-navbar-list .breadcrumb {
display: inline-block; padding: 0;
margin-right: 15px; margin: 0;
background: none;
} }
.summary-navbar-list span a { .summary-navbar-list .breadcrumb > li + li:before {
color: #444;
font-family: FontAwesome;
content: "\f054";
padding: 0 15px;
}
.summary-navbar-list .breadcrumb > .active {
color: #eee;
}
.summary-navbar-list .breadcrumb a {
color: #999; color: #999;
} }
.summary-navbar-list span a:hover { .summary-navbar-list .breadcrumb a:hover {
color: #F9AA03; color: #F9AA03;
} }
.summary-content-title-wrapper { .summary-content-title-wrapper {
@ -1565,19 +1575,19 @@ a:hover .item-children-poster {
top: 3px; top: 3px;
left: 3px; left: 3px;
} }
.user-platforms ul { .user-player ul {
list-style: none; list-style: none;
margin: 0; margin: 0;
} }
.user-platforms-instance { .user-player-instance {
float: left; float: left;
width: 240px; width: 240px;
height: 80px; height: 80px;
margin-bottom: 25px; margin-bottom: 25px;
} }
.user-platforms-instance li { .user-player-instance li {
} }
.user-platforms-instance-box { .user-player-instance-box {
float: left; float: left;
width: 75px; width: 75px;
border-radius: 3px; border-radius: 3px;
@ -1589,7 +1599,7 @@ a:hover .item-children-poster {
height: 80px; height: 80px;
width: 80px; width: 80px;
} }
.user-platforms-instance-name { .user-player-instance-name {
float: left; float: left;
padding-top: 14px; padding-top: 14px;
color: #fff; color: #fff;
@ -1602,7 +1612,7 @@ a:hover .item-children-poster {
width: 140px; width: 140px;
margin-left: 10px; margin-left: 10px;
} }
.user-platforms-instance-playcount h3 { .user-player-instance-playcount h3 {
font-size: 30px; font-size: 30px;
font-weight: bold; font-weight: bold;
color: #F9AA03; color: #F9AA03;
@ -1612,7 +1622,7 @@ a:hover .item-children-poster {
margin: 0 5px 0 10px; margin: 0 5px 0 10px;
float: left; float: left;
} }
.user-platforms-instance-playcount p { .user-player-instance-playcount p {
color: #aaa; color: #aaa;
font-size: 12px; font-size: 12px;
float: left; float: left;

View file

@ -0,0 +1,401 @@
/**
* selectize.bootstrap3.css (v0.12.1) - Bootstrap 3 Theme
* Copyright (c) 20132015 Brian Reavis & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
* @author Brian Reavis <brian@thirdroute.com>
*/
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
visibility: visible !important;
background: #f2f2f2 !important;
background: rgba(0, 0, 0, 0.06) !important;
border: 0 none !important;
-webkit-box-shadow: inset 0 0 12px 4px #ffffff;
box-shadow: inset 0 0 12px 4px #ffffff;
}
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
content: '!';
visibility: hidden;
}
.selectize-control.plugin-drag_drop .ui-sortable-helper {
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.selectize-dropdown-header {
position: relative;
padding: 3px 12px;
border-bottom: 1px solid #d0d0d0;
background: #f8f8f8;
-webkit-border-radius: 4px 4px 0 0;
-moz-border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
}
.selectize-dropdown-header-close {
position: absolute;
right: 12px;
top: 50%;
color: #333333;
opacity: 0.4;
margin-top: -12px;
line-height: 20px;
font-size: 20px !important;
}
.selectize-dropdown-header-close:hover {
color: #000000;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
float: left;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
display: none;
}
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none;
}
.selectize-control.plugin-remove_button [data-value] {
position: relative;
padding-right: 24px !important;
}
.selectize-control.plugin-remove_button [data-value] .remove {
z-index: 1;
/* fixes ie bug (see #392) */
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 17px;
text-align: center;
font-weight: bold;
font-size: 12px;
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 1px 0 0 0;
border-left: 1px solid rgba(0, 0, 0, 0);
-webkit-border-radius: 0 2px 2px 0;
-moz-border-radius: 0 2px 2px 0;
border-radius: 0 2px 2px 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.selectize-control.plugin-remove_button [data-value] .remove:hover {
background: rgba(0, 0, 0, 0.05);
}
.selectize-control.plugin-remove_button [data-value].active .remove {
border-left-color: rgba(0, 0, 0, 0);
}
.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
background: none;
}
.selectize-control.plugin-remove_button .disabled [data-value] .remove {
border-left-color: rgba(77, 77, 77, 0);
}
.selectize-control {
position: relative;
}
.selectize-dropdown,
.selectize-input,
.selectize-input input {
color: #333333;
font-family: inherit;
font-size: inherit;
line-height: 20px;
-webkit-font-smoothing: inherit;
}
.selectize-input,
.selectize-control.single .selectize-input.input-active {
background: #ffffff;
cursor: text;
display: inline-block;
}
.selectize-input {
border: 1px solid #cccccc;
padding: 6px 12px;
display: inline-block;
width: 100%;
overflow: hidden;
position: relative;
z-index: 1;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: none;
box-shadow: none;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.selectize-control.multi .selectize-input.has-items {
padding: 5px 12px 2px;
}
.selectize-input.full {
background-color: #ffffff;
}
.selectize-input.disabled,
.selectize-input.disabled * {
cursor: default !important;
}
.selectize-input.focus {
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
}
.selectize-input.dropdown-active {
-webkit-border-radius: 4px 4px 0 0;
-moz-border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
}
.selectize-input > * {
vertical-align: baseline;
display: -moz-inline-stack;
display: inline-block;
zoom: 1;
*display: inline;
}
.selectize-control.multi .selectize-input > div {
cursor: pointer;
margin: 0 3px 3px 0;
padding: 1px 3px;
background: #efefef;
color: #333333;
border: 0 solid rgba(0, 0, 0, 0);
}
.selectize-control.multi .selectize-input > div.active {
background: #428bca;
color: #ffffff;
border: 0 solid rgba(0, 0, 0, 0);
}
.selectize-control.multi .selectize-input.disabled > div,
.selectize-control.multi .selectize-input.disabled > div.active {
color: #808080;
background: #ffffff;
border: 0 solid rgba(77, 77, 77, 0);
}
.selectize-input > input {
display: inline-block !important;
padding: 0 !important;
min-height: 0 !important;
max-height: none !important;
max-width: 100% !important;
margin: 0 !important;
text-indent: 0 !important;
border: 0 none !important;
background: none !important;
line-height: inherit !important;
-webkit-user-select: auto !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.selectize-input > input::-ms-clear {
display: none;
}
.selectize-input > input:focus {
outline: none !important;
}
.selectize-input::after {
content: ' ';
display: block;
clear: left;
}
.selectize-input.dropdown-active::before {
content: ' ';
display: block;
position: absolute;
background: #ffffff;
height: 1px;
bottom: 0;
left: 0;
right: 0;
}
.selectize-dropdown {
position: absolute;
z-index: 10;
border: 1px solid #d0d0d0;
background: #ffffff;
margin: -1px 0 0 0;
border-top: 0 none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-webkit-border-radius: 0 0 4px 4px;
-moz-border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px;
}
.selectize-dropdown [data-selectable] {
cursor: pointer;
overflow: hidden;
}
.selectize-dropdown [data-selectable] .highlight {
background: rgba(255, 237, 40, 0.4);
-webkit-border-radius: 1px;
-moz-border-radius: 1px;
border-radius: 1px;
}
.selectize-dropdown [data-selectable],
.selectize-dropdown .optgroup-header {
padding: 3px 12px;
}
.selectize-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none;
}
.selectize-dropdown .optgroup-header {
color: #777777;
background: #ffffff;
cursor: default;
}
.selectize-dropdown .active {
background-color: #f5f5f5;
color: #262626;
}
.selectize-dropdown .active.create {
color: #262626;
}
.selectize-dropdown .create {
color: rgba(51, 51, 51, 0.5);
}
.selectize-dropdown-content {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
}
.selectize-control.single .selectize-input,
.selectize-control.single .selectize-input input {
cursor: pointer;
}
.selectize-control.single .selectize-input.input-active,
.selectize-control.single .selectize-input.input-active input {
cursor: text;
}
.selectize-control.single .selectize-input:after {
content: ' ';
display: block;
position: absolute;
top: 50%;
right: 17px;
margin-top: -3px;
width: 0;
height: 0;
border-style: solid;
border-width: 5px 5px 0 5px;
border-color: #333333 transparent transparent transparent;
}
.selectize-control.single .selectize-input.dropdown-active:after {
margin-top: -4px;
border-width: 0 5px 5px 5px;
border-color: transparent transparent #333333 transparent;
}
.selectize-control.rtl.single .selectize-input:after {
left: 17px;
right: auto;
}
.selectize-control.rtl .selectize-input > input {
margin: 0 4px 0 -2px !important;
}
.selectize-control .selectize-input.disabled {
opacity: 0.5;
background-color: #ffffff;
}
.selectize-dropdown,
.selectize-dropdown.form-control {
height: auto;
padding: 0;
margin: 2px 0 0 0;
z-index: 1000;
background: #ffffff;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.selectize-dropdown .optgroup-header {
font-size: 12px;
line-height: 1.42857143;
}
.selectize-dropdown .optgroup:first-child:before {
display: none;
}
.selectize-dropdown .optgroup:before {
content: ' ';
display: block;
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
margin-left: -12px;
margin-right: -12px;
}
.selectize-dropdown-content {
padding: 5px 0;
}
.selectize-dropdown-header {
padding: 6px 12px;
}
.selectize-input {
min-height: 34px;
}
.selectize-input.dropdown-active {
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.selectize-input.dropdown-active::before {
display: none;
}
.selectize-input.focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.has-error .selectize-input {
border-color: #a94442;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.has-error .selectize-input:focus {
border-color: #843534;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
}
.selectize-control.multi .selectize-input.has-items {
padding-left: 9px;
padding-right: 9px;
}
.selectize-control.multi .selectize-input > div {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.form-control.selectize-control {
padding: 0;
height: auto;
border: none;
background: none;
-webkit-box-shadow: none;
box-shadow: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}

View file

@ -137,9 +137,9 @@ DOCUMENTATION :: END
<br /> <br />
% if a['audio_decision'] == 'direct play': % if a['audio_decision'] == 'direct play':
Audio &nbsp;<strong>Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch)</strong> Audio &nbsp;<strong>Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch)</strong>
% elif a['audio_decision'] == 'Copy': % elif a['audio_decision'] == 'copy':
Audio &nbsp;<strong>Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong> Audio &nbsp;<strong>Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
% elif a['audio_decision'] != 'transcode': % elif a['audio_decision'] == 'transcode':
Audio &nbsp;<strong>Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong> Audio &nbsp;<strong>Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
% endif % endif
% elif a['media_type'] == 'episode' or a['media_type'] == 'movie' or a['media_type'] == 'clip': % elif a['media_type'] == 'episode' or a['media_type'] == 'movie' or a['media_type'] == 'clip':

View file

@ -30,6 +30,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="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>
<th align='left' id="paused_counter">Paused</th> <th align='left' id="paused_counter">Paused</th>
@ -84,7 +85,7 @@
} }
} }
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').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, 10] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table); clearSearchButton('history_table', history_table);

View file

@ -16,7 +16,7 @@
<th align='left' id="started">Started</th> <th align='left' id="started">Started</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="friendly_name">User</th> <th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th> <th align='left' id="player">Player</th>
<th align='left' id="title">Title</th> <th align='left' id="title">Title</th>
</tr> </tr>
</thead> </thead>

View file

@ -65,7 +65,7 @@ DOCUMENTATION :: END
%> %>
% if data: % if data:
% if data[0]['rows'] or data[1]['rows'] or data[2]['rows'] or data[3]['rows'] or data[4]['rows'] or data[5]['rows']: % if data[0]['rows']:
<ul class="list-unstyled"> <ul class="list-unstyled">
% for top_stat in data: % for top_stat in data:
% if top_stat['stat_id'] == 'top_tv' and top_stat['rows']: % if top_stat['stat_id'] == 'top_tv' and top_stat['rows']:

View file

@ -60,61 +60,61 @@ DOCUMENTATION :: END
% if data: % if data:
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
% if data['type'] != 'library':
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div> <div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
% endif
<div class="summary-container"> <div class="summary-container">
<div class="summary-navbar"> <div class="summary-navbar">
<div class="col-md-12"> <div class="col-md-12">
<div class="summary-navbar-list"> <div class="summary-navbar-list">
% if data['type'] == 'movie': <ul class="list-unstyled breadcrumb">
<span>Movies</span> % if data['type'] == 'library':
<span><i class="fa fa-chevron-right"></i></span> % if data['library'] == 'movie':
<span><a href="#">${data['title']}</a></span> <li class="active">Movies</li>
% elif data['type'] == 'show': % elif data['library'] == 'show':
<span>TV Shows</span> <li class="active">TV Shows</li>
<span><i class="fa fa-chevron-right"></i></span> % elif data['library'] == 'artist':
<span><a href="#">${data['title']}</a></span> <li class="active">Music</li>
% elif data['type'] == 'season':
<span class="hidden-xs hidden-sm">TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">Season ${data['index']}</a></span>
% elif data['type'] == 'episode':
<span class="hidden-xs hidden-sm">TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">Episode ${data['index']} - ${data['title']}</a></span>
% elif data['type'] == 'artist':
<span>Music</span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">${data['title']}</a></span>
% elif data['type'] == 'album':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">${data['title']}</a></span>
% elif data['type'] == 'track':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">Track ${data['index']} - ${data['title']}</a></span>
% endif % endif
% elif data['type'] == 'movie':
<li><a href="info?item_id=movie">Movies</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'show':
<li><a href="info?item_id=show">TV Shows</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Season ${data['index']}</li>
% elif data['type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></li>
<li class="active">Episode ${data['index']} - ${data['title']}</li>
% elif data['type'] == 'artist':
<li><a href="info?item_id=artist">Music</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Track ${data['index']} - ${data['title']}</li>
% endif
</ul>
</div> </div>
</div> </div>
</div> </div>
% if data['type'] != 'library':
<div class="summary-content-title-wrapper"> <div class="summary-content-title-wrapper">
<div class="col-md-9"> <div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm"> <div class="summary-content-poster hidden-xs hidden-sm">
% if data['type'] == 'track': % if data['type'] == 'track':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web"> <a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web">
% else: % elif data['type'] != 'library':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web"> <a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web">
% endif % endif
% if data['type'] == 'episode': % if data['type'] == 'episode':
@ -129,7 +129,7 @@ DOCUMENTATION :: END
<span></span> <span></span>
</div> </div>
</div> </div>
% else: % elif data['type'] != 'library':
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"> <div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
@ -159,7 +159,9 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% endif
<div class="summary-content-wrapper"> <div class="summary-content-wrapper">
% if data['type'] != 'library':
<div class="col-md-9"> <div class="col-md-9">
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season': % if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div>
@ -311,6 +313,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
% endif % endif
% endif
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
@ -333,6 +336,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="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>
@ -506,7 +510,20 @@ DOCUMENTATION :: END
% if data: % if data:
<script src="interfaces/default/js/tables/history_table.js"></script> <script src="interfaces/default/js/tables/history_table.js"></script>
% if data['type'] == 'show' or data['type'] == 'artist': % if data['type'] == 'library':
<script>
function get_history() {
history_table_options.ajax = {
"url": "get_history",
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'media_type': '${data['media_type']}' };
}
}
}
</script>
% elif data['type'] == 'show' or data['type'] == 'artist':
<script> <script>
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
@ -550,7 +567,7 @@ DOCUMENTATION :: END
$(document).ready(function () { $(document).ready(function () {
get_history(); get_history();
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').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, 10] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table); clearSearchButton('history_table', history_table);
@ -605,7 +622,7 @@ DOCUMENTATION :: END
}); });
</script> </script>
% endif % endif
% if data['rating']: % if data['type'] != 'library' and data['rating']:
<script> <script>
// Convert rating to 5 star rating type // Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2); var starRating = Math.round(${data['rating']} / 2);

File diff suppressed because one or more lines are too long

View file

@ -100,6 +100,17 @@ history_table_options = {
}, },
{ {
"targets": [4], "targets": [4],
"data":"platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "8%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [5],
"data": "player", "data": "player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -114,11 +125,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": "15%", "width": "12%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
}, },
{ {
"targets": [5], "targets": [6],
"data":"full_title", "data":"full_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -145,7 +156,7 @@ history_table_options = {
"width": "35%" "width": "35%"
}, },
{ {
"targets": [6], "targets": [7],
"data":"started", "data":"started",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@ -159,7 +170,7 @@ history_table_options = {
"className": "no-wrap hidden-sm hidden-xs" "className": "no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [7], "targets": [8],
"data":"paused_counter", "data":"paused_counter",
"render": function (data, type, full) { "render": function (data, type, full) {
if (data !== null) { if (data !== null) {
@ -173,7 +184,7 @@ history_table_options = {
"className": "no-wrap hidden-md hidden-sm hidden-xs" "className": "no-wrap hidden-md hidden-sm hidden-xs"
}, },
{ {
"targets": [8], "targets": [9],
"data":"stopped", "data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@ -187,7 +198,7 @@ history_table_options = {
"className": "no-wrap hidden-sm hidden-xs" "className": "no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [9], "targets": [10],
"data":"duration", "data":"duration",
"render": function (data, type, full) { "render": function (data, type, full) {
if (data !== null) { if (data !== null) {
@ -201,15 +212,15 @@ history_table_options = {
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [10], "targets": [11],
"data": "watched_status", "data": "watched_status",
"render": function (data, type, full) { "createdCell": function (td, cellData, rowData, row, col) {
if (data == 1) { if (cellData == 1) {
return '<span class="watched-tooltip" data-toggle="tooltip" title="Watched"><i class="fa fa-lg fa-circle"></i></span>' $(td).html('<span class="watched-tooltip" data-toggle="tooltip" title="' + rowData['percent_complete'] + '%"><i class="fa fa-lg fa-circle"></i></span>');
} else if (data == 0.5) { } else if (cellData == 0.5) {
return '<span class="watched-tooltip" data-toggle="tooltip" title="Partial"><i class="fa fa-lg fa-adjust fa-rotate-180"></i></span>' $(td).html('<span class="watched-tooltip" data-toggle="tooltip" title="' + rowData['percent_complete'] + '%"><i class="fa fa-lg fa-adjust fa-rotate-180"></i></span>');
} else { } else {
return '<span class="watched-tooltip" data-toggle="tooltip" title="Unwatched"><i class="fa fa-lg fa-circle-o"></i></span>' $(td).html('<span class="watched-tooltip" data-toggle="tooltip" title="' + rowData['percent_complete'] + '%"><i class="fa fa-lg fa-circle-o"></i></span>');
} }
}, },
"searchable": false, "searchable": false,
@ -225,12 +236,13 @@ history_table_options = {
// Create the tooltips. // Create the tooltips.
$('.expand-history-tooltip').tooltip({ container: 'body' }); $('.expand-history-tooltip').tooltip({ container: 'body' });
$('.external-ip-tooltip').tooltip(); $('.external-ip-tooltip').tooltip({ container: 'body' });
$('.transcode-tooltip').tooltip(); $('.transcode-tooltip').tooltip({ container: 'body' });
$('.media-type-tooltip').tooltip(); $('.media-type-tooltip').tooltip({ container: 'body' });
$('.watched-tooltip').tooltip(); $('.watched-tooltip').tooltip({ container: 'body' });
$('.thumb-tooltip').popover({ $('.thumb-tooltip').popover({
html: true, html: true,
container: 'body',
trigger: 'hover', trigger: 'hover',
placement: 'right', placement: 'right',
content: function () { content: function () {
@ -462,6 +474,7 @@ 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="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

@ -2,7 +2,7 @@ sync_table_options = {
"processing": false, "processing": false,
"serverSide": false, "serverSide": false,
"pagingType": "bootstrap", "pagingType": "bootstrap",
"order": [ 0, 'desc'], "order": [ [ 0, 'desc'], [ 1, 'asc'], [2, 'asc'] ],
"pageLength": 25, "pageLength": 25,
"stateSave": true, "stateSave": true,
"language": { "language": {
@ -67,13 +67,13 @@ sync_table_options = {
}, },
{ {
"targets": [4], "targets": [4],
"data": "device_name", "data": "platform",
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [5], "targets": [5],
"data": "platform", "data": "device_name",
"className": "no-wrap hidden-sm hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [6], "targets": [6],

View file

@ -50,6 +50,17 @@ user_ip_table_options = {
{ {
"targets": [2], "targets": [2],
"data": "platform", "data": "platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [3],
"data":"player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData) { if (cellData) {
var transcode_dec = ''; var transcode_dec = '';
@ -69,7 +80,7 @@ user_ip_table_options = {
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
}, },
{ {
"targets": [3], "targets": [4],
"data":"last_watched", "data":"last_watched",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -94,10 +105,11 @@ user_ip_table_options = {
} }
} }
}, },
"width": "30%",
"className": "hidden-sm hidden-xs" "className": "hidden-sm hidden-xs"
}, },
{ {
"targets": [4], "targets": [5],
"data":"play_count", "data":"play_count",
"searchable": false, "searchable": false,
"width": "10%" "width": "10%"

View file

@ -64,7 +64,7 @@ users_list_table_options = {
$(td).html(cellData); $(td).html(cellData);
} }
}, },
"width": "12%", "width": "10%",
"className": "edit-user-control no-wrap" "className": "edit-user-control no-wrap"
}, },
{ {
@ -78,7 +78,7 @@ users_list_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"width": "12%", "width": "10%",
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
@ -99,12 +99,25 @@ users_list_table_options = {
$(td).html('n/a'); $(td).html('n/a');
} }
}, },
"width": "12%", "width": "10%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip" "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
}, },
{ {
"targets": [5], "targets": [5],
"data": "platform", "data": "platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
} else {
$(td).html('n/a');
}
},
"width": "10%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [6],
"data":"player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData) { if (cellData) {
var transcode_dec = ''; var transcode_dec = '';
@ -120,11 +133,11 @@ users_list_table_options = {
$(td).html('n/a'); $(td).html('n/a');
} }
}, },
"width": "12%", "width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
}, },
{ {
"targets": [6], "targets": [7],
"data":"last_watched", "data":"last_watched",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -153,7 +166,7 @@ users_list_table_options = {
"className": "hidden-sm hidden-xs" "className": "hidden-sm hidden-xs"
}, },
{ {
"targets": [7], "targets": [8],
"data": "plays", "data": "plays",
"searchable": false, "searchable": false,
"width": "10%" "width": "10%"

View file

@ -37,7 +37,13 @@ DOCUMENTATION :: END
<li> <li>
<div class="home-platforms-instance-info"> <div class="home-platforms-instance-info">
<div class="home-platforms-instance-name"> <div class="home-platforms-instance-name">
% if library['type'] != 'photo':
<h4>
<a href="info?item_id=${library['type']}" title="${library['rows']['title']}">${library['rows']['title']}</a>
</h4>
% else:
<h4>${library['rows']['title']}</h4> <h4>${library['rows']['title']}</h4>
% endif
</div> </div>
<div class="home-platforms-instance-playcount"> <div class="home-platforms-instance-playcount">
<h5>${library['rows']['count_type']}</h5> <h5>${library['rows']['count_type']}</h5>

View file

@ -44,6 +44,24 @@ from plexpy import helpers
<p class="help-block">${item['description']}</p> <p class="help-block">${item['description']}</p>
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}"> <input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</div> </div>
% elif item['input_type'] == 'select':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-5">
<select class="form-control" id="${item['name']}" name="${item['name']}" >
% for key, value in sorted(item['select_options'].iteritems()):
% if key == item['value']:
<option value="${key}" selected>${value}</option>
% else:
<option value="${key}">${value}</option>
% endif
% endfor
</select>
</div>
</div>
<p class="help-block">${item['description']}</p>
</div>
% endif % endif
% endfor % endfor
</div> </div>
@ -68,29 +86,38 @@ from plexpy import helpers
<script> <script>
$('#osxnotifyregister').click(function () { $('#osxnotifyregister').click(function () {
var osx_notify_app = $("#osx_notify_app").val(); var osx_notify_app = $("#osx_notify_app").val();
$.get("/osxnotifyregister", {'app': osx_notify_app}, function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); }); $.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut() $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
}) })
var notificationConfig = $("#set_notification_config");
$('#save-notification-item').click(function() { $('#save-notification-item').click(function() {
doAjaxCall('set_notification_config', $(this), 'tabs', true); doAjaxCall('set_notification_config', $(this), 'tabs', true);
// Reload modal to update certain fields
$.ajax({
url: 'get_notification_agent_config',
data: { config_id: '${config_id}' },
cache: false,
async: true,
complete: function (xhr, status) {
$("#notification-config-modal").html(xhr.responseText);
}
});
return false; return false;
}); });
$('#twitterStep1').click(function () { $('#twitterStep1').click(function () {
$.get("/twitterStep1", function (data) {window.open(data); }) $.get("/twitterStep1", function (data) {window.open(data); })
.done(function () { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>Confirm Authorization. Check pop-up blocker if no response.</div>"); }); .done(function () { $('#ajaxMsg').html("<i class='fa fa-check'></i> Confirm Authorization. Check pop-up blocker if no response."); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
}); });
$('#twitterStep2').click(function () { $('#twitterStep2').click(function () {
var twitter_key = $("#twitter_key").val(); var twitter_key = $("#twitter_key").val();
$.get("/twitterStep2", {'key': twitter_key}, function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); }); $.get("/twitterStep2", { 'key': twitter_key }, function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
}); });
$('#testTwitter').click(function () { $('#testTwitter').click(function () {
$.get("/testTwitter", $.get("/testTwitter",
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); }); function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
}); });

View file

@ -26,11 +26,11 @@
<thead> <thead>
<tr> <tr>
<th align='left' id="state">State</th> <th align='left' id="state">State</th>
<th align='left' id="username">Username</th> <th align='left' id="user">User</th>
<th align='left' id="title">Title</th> <th align='left' id="title">Title</th>
<th align='left' id="type">Type</th> <th align='left' id="type">Type</th>
<th align='left' id="device">Device</th>
<th align='left' id="platform">Platform</th> <th align='left' id="platform">Platform</th>
<th align='left' id="device">Device</th>
<th align='left' id="size">Total Size</th> <th align='left' id="size">Total Size</th>
<th align='left' id="items">Total Items</th> <th align='left' id="items">Total Items</th>
<th align='left' id="converted">Converted</th> <th align='left' id="converted">Converted</th>

View file

@ -85,11 +85,11 @@ from plexpy import helpers
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-header"> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-television"></i> Platform Stats</span> <span><i class="fa fa-television"></i> Player Stats</span>
</div> </div>
</div> </div>
<div class="table-card-back"> <div class="table-card-back">
<div id="user-platform-stats" class="user-platforms"> <div id="user-player-stats" class="user-player">
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div> <div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div>
<br> <br>
</div> </div>
@ -133,6 +133,7 @@ from plexpy import helpers
<th align="left">Last Seen</th> <th align="left">Last Seen</th>
<th align="left">IP Address</th> <th align="left">IP Address</th>
<th align="left">Last Platform</th> <th align="left">Last Platform</th>
<th align="left">Last Player</th>
<th align="left">Last Watched</th> <th align="left">Last Watched</th>
<th align="left">Play Count</th> <th align="left">Play Count</th>
</tr> </tr>
@ -170,6 +171,7 @@ from plexpy import helpers
<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="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>
@ -207,8 +209,8 @@ from plexpy import helpers
<th align='left' id="username">Username</th> <th align='left' id="username">Username</th>
<th align='left' id="sync_title">Title</th> <th align='left' id="sync_title">Title</th>
<th align='left' id="type">Type</th> <th align='left' id="type">Type</th>
<th align='left' id="device">Device</th>
<th align='left' id="sync_platform">Platform</th> <th align='left' id="sync_platform">Platform</th>
<th align='left' id="device">Device</th>
<th align='left' id="size">Total Size</th> <th align='left' id="size">Total Size</th>
<th align='left' id="items">Total Items</th> <th align='left' id="items">Total Items</th>
<th align='left' id="converted">Converted</th> <th align='left' id="converted">Converted</th>
@ -309,11 +311,11 @@ from plexpy import helpers
// Populate platform stats // Populate platform stats
$.ajax({ $.ajax({
url: 'get_user_platform_stats', url: 'get_user_player_stats',
async: true, async: true,
data: { user_id: user_id, user: '${data['username']}' }, data: { user_id: user_id, user: '${data['username']}' },
complete: function(xhr, status) { complete: function(xhr, status) {
$("#user-platform-stats").html(xhr.responseText); $("#user-player-stats").html(xhr.responseText);
} }
}); });
@ -332,7 +334,7 @@ from plexpy import helpers
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').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, 10] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table); clearSearchButton('history_table', history_table);

View file

@ -11,8 +11,9 @@ data[array_index] :: Usable parameters
== Global keys == == Global keys ==
result_id Returns a unique identifier for the result. result_id Returns a unique identifier for the result.
platform_name Returns the name of the platform. player_name Returns the name of the player.
total_plays Returns the play count for the platform. platform_type Returns the name of the platform
total_plays Returns the play count for the player.
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%doc>
@ -20,13 +21,13 @@ DOCUMENTATION :: END
% if data != None: % if data != None:
% for a in data: % for a in data:
<ul class="list-unstyled"> <ul class="list-unstyled">
<div class="user-platforms-instance"> <div class="user-player-instance">
<li> <li>
<span id="user-platform-image-${a['result_id']}"></span> <span id="user-player-image-${a['result_id']}"></span>
<div class="user-platforms-instance-name"> <div class="user-player-instance-name">
${a['platform_name']} ${a['player_name']}
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-player-instance-playcount">
<h3>${a['total_plays']}</h3> <h3>${a['total_plays']}</h3>
<p> plays</p> <p> plays</p>
</div> </div>
@ -34,7 +35,7 @@ DOCUMENTATION :: END
</div> </div>
</ul> </ul>
<script> <script>
$("#user-platform-image-${a['result_id']}").html("<div class='user-platforms-instance-box' style='background-image: url(" + getPlatformImagePath('${a['platform_type']}') + ");'>"); $("#user-player-image-${a['result_id']}").html("<div class='user-player-instance-box' style='background-image: url(" + getPlatformImagePath('${a['platform_type']}') + ");'>");
</script> </script>
% endfor % endfor
% else: % else:

View file

@ -29,6 +29,7 @@
<th align="left" id="last_seen">Last Seen</th> <th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_known_ip">Last Known IP</th> <th align="left" id="last_known_ip">Last Known IP</th>
<th align="left" id="last_platform">Last Platform</th> <th align="left" id="last_platform">Last Platform</th>
<th align="left" id="last_player">Last Player</th>
<th align="left" id="last_watched">Last Watched</th> <th align="left" id="last_watched">Last Watched</th>
<th align="left" id="total_plays">Total Plays</th> <th align="left" id="total_plays">Total Plays</th>
</tr> </tr>

View file

@ -15,6 +15,7 @@ from plexpy import common
<link href="interfaces/default/css/bootstrap3/bootstrap.css" rel="stylesheet"> <link href="interfaces/default/css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="interfaces/default/css/bootstrap-wizard.css" rel="stylesheet"> <link href="interfaces/default/css/bootstrap-wizard.css" rel="stylesheet">
<link href="interfaces/default/css/plexpy.css" rel="stylesheet"> <link href="interfaces/default/css/plexpy.css" rel="stylesheet">
<link href="interfaces/default/css/selectize.bootstrap3.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css"> <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
<link href="interfaces/default/css/font-awesome.min.css" rel="stylesheet"> <link href="interfaces/default/css/font-awesome.min.css" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="interfaces/default/images/favicon.ico"/> <link rel="icon" type="image/x-icon" href="interfaces/default/images/favicon.ico"/>
@ -40,45 +41,6 @@ from plexpy import common
</div> </div>
</div> </div>
<div class="wizard-card" data-cardname="card2"> <div class="wizard-card" data-cardname="card2">
<h3>Plex Media Server</h3>
<form>
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure PlexPy can reach the server.</p>
<div class="wizard-input-section">
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control pms-settings" name="pms_ip" id="pms_ip" placeholder="127.0.0.1" value="${config['pms_ip']}" required>
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="pms_port">Port Number</label>
<div class="row">
<div class="col-xs-3">
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Force SSL
</label>
</div>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1" ${config['pms_is_remote']}> Remote Server
</label>
</div>
</div>
</div>
</div>
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div>
<div class="wizard-card" data-cardname="card3">
<h3>Plex Authentication</h3> <h3>Plex Authentication</h3>
<p class="help-block">Enter your Plex.tv username and password. PlexPy does not store your username or password.</p> <p class="help-block">Enter your Plex.tv username and password. PlexPy does not store your username or password.</p>
<div class="wizard-input-section"> <div class="wizard-input-section">
@ -100,6 +62,46 @@ from plexpy import common
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken"> <input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken">
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span> <a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
</div> </div>
<div class="wizard-card" data-cardname="card3">
<h3>Plex Media Server</h3>
<form>
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure PlexPy can reach the server.</p>
<div class="wizard-input-section">
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
<div class="col-xs-8">
<select id="pms_ip" name="pms_ip"></select>
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="pms_port">Port Number</label>
<div class="row">
<div class="col-xs-3">
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Force SSL
</label>
</div>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1"> Remote Server
</label>
</div>
</div>
</div>
</div>
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div>
<div class="wizard-card" data-cardname="card4"> <div class="wizard-card" data-cardname="card4">
<h3>Monitoring</h3> <h3>Monitoring</h3>
<div class="wizard-input-section"> <div class="wizard-input-section">
@ -184,6 +186,7 @@ from plexpy import common
<script src="interfaces/default/js/jquery-2.1.4.min.js"></script> <script src="interfaces/default/js/jquery-2.1.4.min.js"></script>
<script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script> <script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script>
<script src="interfaces/default/js/selectize.min.js"></script>
<script src="interfaces/default/js/script.js"></script> <script src="interfaces/default/js/script.js"></script>
<script src="interfaces/default/js/bootstrap-wizard.min.js"></script> <script src="interfaces/default/js/bootstrap-wizard.min.js"></script>
<script> <script>
@ -218,6 +221,73 @@ from plexpy import common
} }
}) })
}); });
$select_pms = $('#pms_ip').selectize({
create: true,
createOnBlur: true,
openOnFocus: true,
maxItems: 1,
closeAfterSelect: true,
onInitialize: function () {
var s = this;
this.revertSettings.$children.each(function () {
$.extend(s.options[this.value], $(this).data());
});
},
render: {
option: function (item, escape) {
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
},
item: function (item, escape) {
// first item is rendered before initialization bug?
if (!item.ci) {
$.extend(item,
$(this.revertSettings.$children)
.filter('[value="' + item.value + '"]').data());
}
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
}
},
onChange: function (item) {
var ci = $('.selectize-input').find('div').attr('data-ci');
var port = $('.selectize-input').find('div').attr('data-port')
var local = $('.selectize-input').find('div').attr('data-local')
var ssl = $('.selectize-input').find('div').attr('data-use_ssl')
$("#pms-verify-status").html("");
// If a option was added by a user its
// data-xxx="undefined"
if (ci != "undefined") {
// To allow next step in the guide.
// servers with clientIdentifier is verified
$("#pms_valid").val("valid");
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
} else {
// Self made options must be verified
$("#pms_valid").val("");
$("#pms-verify-status").html("").hide();
}
// If the server is verified set the correct port
if (port != "undefined") {
$('#pms_port').val(port);
} else {
// set default port
$('#pms_port').val("32400");
}
if (local != "undefined" && local == '0') {
$('#pms_is_remote').prop('checked', true);
} else {
$('#pms_is_remote').prop('checked', false);
}
if (ssl != "undefined" && ssl == "1") {
$('#pms_ssl').prop('checked', true);
} else {
$('#pms_ssl').prop('checked', false);
}
}
});
}); });
@ -371,6 +441,7 @@ from plexpy import common
$('#pms-token-status').fadeIn('fast'); $('#pms-token-status').fadeIn('fast');
$("#pms_token").val(authToken); $("#pms_token").val(authToken);
authenticated = true; authenticated = true;
getServerOptions(authToken)
} }
}); });
} else { } else {
@ -399,6 +470,18 @@ from plexpy import common
} }
}); });
}); });
function getServerOptions(token) {
/* Set token and returns server options */
$.ajax({
url: "discover/" + token,
success: function (result) {
$('#pms_ip').html("")
// Add all servers to the "combobox"
$select_pms[0].selectize.addOption(result);
}
})
}
</script> </script>
</body> </body>

View file

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
@ -13,86 +16,143 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import db, cache, versioncheck, logger, helpers from plexpy import versioncheck, logger, plextv, pmsconnect, datafactory, graphs, users
import os
import plexpy import plexpy
import json import json
from xml.dom import minidom import traceback
import cherrypy
import re
import hashlib
import random
import xmltodict
cmd_list = ['getHistory', 'getLogs', 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update'] cmd_list = ['getLogs', 'getVersion', 'checkGithub', 'shutdown',
'getSettings', 'restart', 'update', 'getApikey', 'getHistory',
'getMetadata', 'getUserips', 'getPlayby', 'getSync']
class Api(object): class Api(object):
def __init__(self, out='json'):
def __init__(self):
self.apikey = None self.apikey = None
self.authenticated = False
self.cmd = None self.cmd = None
self.id = None
self.kwargs = None self.kwargs = None
# For the responses
self.data = None self.data = None
self.msg = None
self.result_type = 'error'
# Possible general params
self.callback = None self.callback = None
self.out_type = out
self.debug = None
def checkParams(self, *args, **kwargs): def checkParams(self, *args, **kwargs):
if not plexpy.CONFIG.API_ENABLED: if not plexpy.CONFIG.API_ENABLED:
self.data = 'API not enabled' self.msg = 'API not enabled'
return elif not plexpy.CONFIG.API_KEY:
if not plexpy.CONFIG.API_KEY: self.msg = 'API key not generated'
self.data = 'API key not generated' elif len(plexpy.CONFIG.API_KEY) != 32:
return self.msg = 'API key not generated correctly'
if len(plexpy.CONFIG.API_KEY) != 32: elif 'apikey' not in kwargs:
self.data = 'API key not generated correctly' self.msg = 'Parameter apikey is required'
return elif kwargs.get('apikey', '') != plexpy.CONFIG.API_KEY:
self.msg = 'Invalid apikey'
elif 'cmd' not in kwargs:
self.msg = 'Parameter %s required. possible commands are: %s' % ', '.join(cmd_list)
elif 'cmd' in kwargs and kwargs.get('cmd') not in cmd_list:
self.msg = 'Unknown command, %s possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(cmd_list))
if 'apikey' not in kwargs: # Set default values or remove them from kwargs
self.data = 'Missing api key'
return
if kwargs['apikey'] != plexpy.CONFIG.API_KEY: self.callback = kwargs.pop('callback', None)
self.data = 'Incorrect API key' self.apikey = kwargs.pop('apikey', None)
return self.cmd = kwargs.pop('cmd', None)
else: self.debug = kwargs.pop('debug', False)
self.apikey = kwargs.pop('apikey') # Allow override for the api.
self.out_type = kwargs.pop('out_type', 'json')
if 'cmd' not in kwargs: if self.apikey == plexpy.CONFIG.API_KEY and plexpy.CONFIG.API_ENABLED and self.cmd in cmd_list:
self.data = 'Missing parameter: cmd' self.authenticated = True
return self.msg = None
elif self.cmd == 'getApikey' and plexpy.CONFIG.API_ENABLED:
if kwargs['cmd'] not in cmd_list: self.authenticated = True
self.data = 'Unknown command: %s' % kwargs['cmd'] # Remove the old error msg
return self.msg = None
else:
self.cmd = kwargs.pop('cmd')
self.kwargs = kwargs self.kwargs = kwargs
self.data = 'OK'
def _responds(self, result_type='success', data=None, msg=''):
if data is None:
data = {}
return {"response": {"result": result_type, "message": msg, "data": data}}
def _out_as(self, out):
if self.out_type == 'json':
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
try:
out = json.dumps(out, indent=4, sort_keys=True)
if self.callback is not None:
cherrypy.response.headers['Content-Type'] = 'application/javascript'
# wrap with JSONP call if requested
out = self.callback + '(' + out + ');'
# if we fail to generate the output fake an error
except Exception as e:
logger.info(u"API :: " + traceback.format_exc())
out['message'] = traceback.format_exc()
out['result'] = 'error'
if self.out_type == 'xml':
cherrypy.response.headers['Content-Type'] = 'application/xml'
try:
out = xmltodict.unparse(out, pretty=True)
except ValueError as e:
logger.error('Failed to parse xml result')
try:
out['message'] = e
out['result'] = 'error'
out = xmltodict.unparse(out, pretty=True)
except Exception as e:
logger.error('Failed to parse xml result error message')
out = '''<?xml version="1.0" encoding="utf-8"?>
<response>
<message>%s</message>
<data></data>
<result>error</result>
</response>
''' % e
return out
def fetchData(self): def fetchData(self):
if self.data == 'OK': logger.info('Recieved API command: %s' % self.cmd)
logger.info('Recieved API command: %s', self.cmd) if self.cmd and self.authenticated:
methodToCall = getattr(self, "_" + self.cmd) methodtocall = getattr(self, "_" + self.cmd)
methodToCall(**self.kwargs) # Let the traceback hit cherrypy so we can
if 'callback' not in self.kwargs: # see the traceback there
if isinstance(self.data, basestring): if self.debug:
return self.data methodtocall(**self.kwargs)
else: else:
return json.dumps(self.data) try:
else: methodtocall(**self.kwargs)
self.callback = self.kwargs['callback'] except Exception as e:
self.data = json.dumps(self.data) logger.error(traceback.format_exc())
self.data = self.callback + '(' + self.data + ');'
return self.data # Im just lazy, fix me plx
else: if self.data or isinstance(self.data, (dict, list)):
return self.data if len(self.data):
self.result_type = 'success'
return self._out_as(self._responds(result_type=self.result_type, msg=self.msg, data=self.data))
def _dic_from_query(self, query): def _dic_from_query(self, query):
myDB = db.DBConnection() myDB = database.DBConnection()
rows = myDB.select(query) rows = myDB.select(query)
rows_as_dic = [] rows_as_dic = []
@ -103,104 +163,115 @@ class Api(object):
return rows_as_dic return rows_as_dic
def _getHistory(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', sSortDir_0='asc', **kwargs): def _getApikey(self, username='', password=''):
iDisplayStart = int(iDisplayStart) """ Returns api key, requires username and password is active """
iDisplayLength = int(iDisplayLength)
filtered = []
totalcount = 0
myDB = db.DBConnection()
db_table = db.DBConnection().get_history_table_name()
sortcolumn = 'time' apikey = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32]
sortbyhavepercent = False if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD:
if iSortCol_0 == '1': if username == plexpy.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
sortcolumn = 'user' if plexpy.CONFIG.API_KEY:
if iSortCol_0 == '2': self.data = plexpy.CONFIG.API_KEY
sortcolumn = 'platform'
elif iSortCol_0 == '3':
sortcolumn = 'ip_address'
elif iSortCol_0 == '4':
sortcolumn = 'title'
elif iSortCol_0 == '5':
sortcolumn = 'time'
elif iSortCol_0 == '6':
sortcolumn = 'paused_counter'
elif iSortCol_0 == '7':
sortcolumn = 'stopped'
elif iSortCol_0 == '8':
sortbyhavepercent = True
if sSearch == "":
query = 'SELECT * from %s order by %s COLLATE NOCASE %s' % (db_table, sortcolumn, sSortDir_0)
filtered = myDB.select(query)
totalcount = len(filtered)
else: else:
query = 'SELECT * from ' + db_table + ' WHERE user LIKE "%' + sSearch + \ self.data = apikey
'%" OR title LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (sortcolumn, sSortDir_0) plexpy.CONFIG.API_KEY = apikey
filtered = myDB.select(query) plexpy.CONFIG.write()
totalcount = myDB.select('SELECT COUNT(*) from processed')[0][0] else:
self.msg = 'Authentication is enabled, please add the correct username and password to the parameters'
else:
if plexpy.CONFIG.API_KEY:
self.data = plexpy.CONFIG.API_KEY
else:
# Make a apikey if the doesn't exist
self.data = apikey
plexpy.CONFIG.API_KEY = apikey
plexpy.CONFIG.write()
history = filtered[iDisplayStart:(iDisplayStart + iDisplayLength)] return self.data
rows = []
for item in history: def _getLogs(self, sort='', search='', order='desc', regex='', **kwargs):
row = {"date": item['time'], """
"user": item["user"], Returns the log
"platform": item["platform"],
"ip_address": item["ip_address"], Returns [{"response":
"title": item["title"], {"msg": "Hey",
"started": item["time"], "result": "success"},
"paused": item["paused_counter"], "data": [{"time": "29-sept.2015",
"stopped": item["stopped"], "thread: "MainThread",
"duration": "", "msg: "Called x from y",
"percent_complete": 0, "loglevel": "DEBUG"
} }
]
if item['paused_counter'] > 0: }
row['paused'] = item['paused_counter'] ]
else: """
row['paused'] = 0 logfile = os.path.join(plexpy.CONFIG.LOG_DIR, 'plexpy.log')
templog = []
start = int(kwargs.get('start', 0))
end = int(kwargs.get('end', 0))
if item['time']: if regex:
if item['stopped'] > 0: logger.debug('Filtering log using regex %s' % regex)
stopped = item['stopped'] reg = re.compile('u' + regex, flags=re.I)
else:
stopped = 0
if item['paused_counter'] > 0:
paused_counter = item['paused_counter']
else:
paused_counter = 0
row['duration'] = stopped - item['time'] + paused_counter for line in open(logfile, 'r').readlines():
temp_loglevel_and_time = None
try: try:
xml_parse = minidom.parseString(helpers.latinToAscii(item['xml'])) temp_loglevel_and_time = line.split('- ')
except IOError, e: loglvl = temp_loglevel_and_time[1].split(' :')[0].strip()
logger.warn("Error parsing XML in PlexWatch db: %s" % e) tl_tread = line.split(' :: ')
if loglvl is None:
xml_head = xml_parse.getElementsByTagName('opt') msg = line.replace('\n', '')
if not xml_head:
logger.warn("Error parsing XML in PlexWatch db: %s" % e)
for s in xml_head:
if s.getAttribute('duration') and s.getAttribute('viewOffset'):
view_offset = helpers.cast_to_float(s.getAttribute('viewOffset'))
duration = helpers.cast_to_float(s.getAttribute('duration'))
if duration > 0:
row['percent_complete'] = (view_offset / duration)*100
else: else:
row['percent_complete'] = 0 msg = line.split(' : ')[1].replace('\n', '')
thread = tl_tread[1].split(' : ')[0]
except IndexError:
# We assume this is a traceback
tl = (len(templog) - 1)
templog[tl]['msg'] += line.replace('\n', '')
continue
rows.append(row) if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
dict = {'iTotalDisplayRecords': len(filtered), d = {
'iTotalRecords': totalcount, 'time': temp_loglevel_and_time[0],
'aaData': rows, 'loglevel': loglvl,
'msg': msg.replace('\n', ''),
'thread': thread
} }
self.data = json.dumps(dict) templog.append(d)
#cherrypy.response.headers['Content-type'] = 'application/json'
def _getLogs(self, **kwargs): if end > 0:
pass logger.debug('Slicing the log from %s to %s' % (start, end))
templog = templog[start:end]
if sort:
logger.debug('Sorting log based on %s' % sort)
templog = sorted(templog, key=lambda k: k[sort])
if search:
logger.debug('Searching log values for %s' % search)
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
if len(tt):
templog = tt
if regex:
tt = []
for l in templog:
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
if reg.search(stringdict):
tt.append(l)
if len(tt):
templog = tt
if order == 'desc':
templog = templog[::-1]
self.data = templog
return templog
def _getVersion(self, **kwargs): def _getVersion(self, **kwargs):
self.data = { self.data = {
@ -210,6 +281,7 @@ class Api(object):
'latest_version': plexpy.LATEST_VERSION, 'latest_version': plexpy.LATEST_VERSION,
'commits_behind': plexpy.COMMITS_BEHIND, 'commits_behind': plexpy.COMMITS_BEHIND,
} }
self.result_type = 'success'
def _checkGithub(self, **kwargs): def _checkGithub(self, **kwargs):
versioncheck.checkGithub() versioncheck.checkGithub()
@ -217,9 +289,211 @@ class Api(object):
def _shutdown(self, **kwargs): def _shutdown(self, **kwargs):
plexpy.SIGNAL = 'shutdown' plexpy.SIGNAL = 'shutdown'
self.msg = 'Shutting down plexpy'
self.result_type = 'success'
def _restart(self, **kwargs): def _restart(self, **kwargs):
plexpy.SIGNAL = 'restart' plexpy.SIGNAL = 'restart'
self.msg = 'Restarting plexpy'
self.result_type = 'success'
def _update(self, **kwargs): def _update(self, **kwargs):
plexpy.SIGNAL = 'update' plexpy.SIGNAL = 'update'
self.msg = 'Updating plexpy'
self.result_type = 'success'
def _getHistory(self, user=None, user_id=None, rating_key='', parent_rating_key='', grandparent_rating_key='', start_date='', **kwargs):
custom_where = []
if user_id:
custom_where = [['user_id', user_id]]
elif user:
custom_where = [['user', user]]
if 'rating_key' in kwargs:
rating_key = kwargs.get('rating_key', "")
custom_where = [['rating_key', rating_key]]
if 'parent_rating_key' in kwargs:
rating_key = kwargs.get('parent_rating_key', "")
custom_where = [['parent_rating_key', rating_key]]
if 'grandparent_rating_key' in kwargs:
rating_key = kwargs.get('grandparent_rating_key', "")
custom_where = [['grandparent_rating_key', rating_key]]
if 'start_date' in kwargs:
start_date = kwargs.get('start_date', "")
custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]]
data_factory = datafactory.DataFactory()
history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where)
self.data = history
return self.data
def _getSync(self, machine_id=None, user_id=None, **kwargs):
pms_connect = pmsconnect.PmsConnect()
server_id = pms_connect.get_server_identity()
plex_tv = plextv.PlexTV()
if not machine_id:
result = plex_tv.get_synced_items(machine_id=server_id['machine_identifier'], user_id=user_id)
else:
result = plex_tv.get_synced_items(machine_id=machine_id, user_id=user_id)
if result:
self.data = result
return result
else:
self.msg = 'Unable to retrieve sync data for user'
logger.warn('Unable to retrieve sync data for user.')
def _getMetadata(self, rating_key='', **kwargs):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_metadata(rating_key, 'dict')
if result:
self.data = result
return result
else:
self.msg = 'Unable to retrive metadata %s' % rating_key
logger.warn('Unable to retrieve data.')
def _getSettings(self):
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
interface_list = [name for name in os.listdir(interface_dir) if
os.path.isdir(os.path.join(interface_dir, name))]
config = {
"http_host": plexpy.CONFIG.HTTP_HOST,
"http_username": plexpy.CONFIG.HTTP_USERNAME,
"http_port": plexpy.CONFIG.HTTP_PORT,
"http_password": plexpy.CONFIG.HTTP_PASSWORD,
"launch_browser": bool(plexpy.CONFIG.LAUNCH_BROWSER),
"enable_https": bool(plexpy.CONFIG.ENABLE_HTTPS),
"https_cert": plexpy.CONFIG.HTTPS_CERT,
"https_key": plexpy.CONFIG.HTTPS_KEY,
"api_enabled": plexpy.CONFIG.API_ENABLED,
"api_key": plexpy.CONFIG.API_KEY,
"update_db_interval": plexpy.CONFIG.UPDATE_DB_INTERVAL,
"freeze_db": bool(plexpy.CONFIG.FREEZE_DB),
"log_dir": plexpy.CONFIG.LOG_DIR,
"cache_dir": plexpy.CONFIG.CACHE_DIR,
"check_github": bool(plexpy.CONFIG.CHECK_GITHUB),
"interface_list": interface_list,
"cache_sizemb": plexpy.CONFIG.CACHE_SIZEMB,
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
"pms_ip": plexpy.CONFIG.PMS_IP,
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
"pms_port": plexpy.CONFIG.PMS_PORT,
"pms_token": plexpy.CONFIG.PMS_TOKEN,
"pms_ssl": bool(plexpy.CONFIG.PMS_SSL),
"pms_use_bif": bool(plexpy.CONFIG.PMS_USE_BIF),
"pms_uuid": plexpy.CONFIG.PMS_UUID,
"date_format": plexpy.CONFIG.DATE_FORMAT,
"time_format": plexpy.CONFIG.TIME_FORMAT,
"grouping_global_history": bool(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
"grouping_user_history": bool(plexpy.CONFIG.GROUPING_USER_HISTORY),
"grouping_charts": bool(plexpy.CONFIG.GROUPING_CHARTS),
"tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": bool(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"music_notify_enable": bool(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": bool(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
"music_notify_on_start": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_START),
"tv_notify_on_stop": bool(plexpy.CONFIG.TV_NOTIFY_ON_STOP),
"movie_notify_on_stop": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_STOP),
"music_notify_on_stop": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_STOP),
"tv_notify_on_pause": bool(plexpy.CONFIG.TV_NOTIFY_ON_PAUSE),
"movie_notify_on_pause": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_PAUSE),
"music_notify_on_pause": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_PAUSE),
"monitoring_interval": plexpy.CONFIG.MONITORING_INTERVAL,
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
"refresh_users_on_startup": bool(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
"ip_logging_enable": bool(plexpy.CONFIG.IP_LOGGING_ENABLE),
"video_logging_enable": bool(plexpy.CONFIG.VIDEO_LOGGING_ENABLE),
"music_logging_enable": bool(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"pms_is_remote": bool(plexpy.CONFIG.PMS_IS_REMOTE),
"notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT,
"notify_on_start_subject_text": plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT,
"notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT,
"notify_on_stop_subject_text": plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT,
"notify_on_stop_body_text": plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT,
"notify_on_pause_subject_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT,
"notify_on_pause_body_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT,
"notify_on_resume_subject_text": plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT,
"notify_on_resume_body_text": plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT,
"notify_on_buffer_subject_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT,
"notify_on_buffer_body_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT,
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": bool(plexpy.CONFIG.HOME_STATS_TYPE),
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
"home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS,
"home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS,
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT
}
self.data = config
return config
def _getUserips(self, user_id=None, user=None, **kwargs):
custom_where = []
if user_id:
custom_where = [['user_id', user_id]]
elif user:
custom_where = [['user', user]]
user_data = users.Users()
history = user_data.get_user_unique_ips(kwargs=kwargs,
custom_where=custom_where)
if history:
self.data = history
return history
else:
self.msg = 'Failed to find users ips'
def _getPlayby(self, time_range='30', y_axis='plays', playtype='total_plays_per_month', **kwargs):
graph = graphs.Graphs()
if playtype == 'total_plays_per_month':
result = graph.get_total_plays_per_month(y_axis=y_axis)
elif playtype == 'total_plays_per_day':
result = graph.get_total_plays_per_day(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_per_hourofday':
result = graph.get_total_plays_per_hourofday(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_per_dayofweek':
result = graph.get_total_plays_per_dayofweek(time_range=time_range, y_axis=y_axis)
elif playtype == 'stream_type_by_top_10_users':
result = graph.get_stream_type_by_top_10_users(time_range=time_range, y_axis=y_axis)
elif playtype == 'stream_type_by_top_10_platforms':
result = graph.get_stream_type_by_top_10_platforms(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_by_stream_resolution':
result = graph.get_total_plays_by_stream_resolution(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_by_source_resolution':
result = graph.get_total_plays_by_source_resolution(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_per_stream_type':
result = graph.get_total_plays_per_stream_type(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_by_top_10_users':
result = graph.get_total_plays_by_top_10_users(time_range=time_range, y_axis=y_axis)
elif playtype == 'total_plays_by_top_10_platforms':
result = graph.get_total_plays_by_top_10_platforms(time_range=time_range, y_axis=y_axis)
if result:
self.data = result
return result
else:
logger.warn('Unable to retrieve %s from db' % playtype)

View file

@ -37,6 +37,7 @@ _CONFIG_DEFINITIONS = {
'API_KEY': (str, 'General', ''), 'API_KEY': (str, 'General', ''),
'BOXCAR_ENABLED': (int, 'Boxcar', 0), 'BOXCAR_ENABLED': (int, 'Boxcar', 0),
'BOXCAR_TOKEN': (str, 'Boxcar', ''), 'BOXCAR_TOKEN': (str, 'Boxcar', ''),
'BOXCAR_SOUND': (str, 'Boxcar', ''),
'BOXCAR_ON_PLAY': (int, 'Boxcar', 0), 'BOXCAR_ON_PLAY': (int, 'Boxcar', 0),
'BOXCAR_ON_STOP': (int, 'Boxcar', 0), 'BOXCAR_ON_STOP': (int, 'Boxcar', 0),
'BOXCAR_ON_PAUSE': (int, 'Boxcar', 0), 'BOXCAR_ON_PAUSE': (int, 'Boxcar', 0),
@ -185,6 +186,7 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_ENABLED': (int, 'Pushover', 0), 'PUSHOVER_ENABLED': (int, 'Pushover', 0),
'PUSHOVER_KEYS': (str, 'Pushover', ''), 'PUSHOVER_KEYS': (str, 'Pushover', ''),
'PUSHOVER_PRIORITY': (int, 'Pushover', 0), 'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'PUSHOVER_SOUND': (str, 'Pushover', ''),
'PUSHOVER_ON_PLAY': (int, 'Pushover', 0), 'PUSHOVER_ON_PLAY': (int, 'Pushover', 0),
'PUSHOVER_ON_STOP': (int, 'Pushover', 0), 'PUSHOVER_ON_STOP': (int, 'Pushover', 0),
'PUSHOVER_ON_PAUSE': (int, 'Pushover', 0), 'PUSHOVER_ON_PAUSE': (int, 'Pushover', 0),

View file

@ -42,6 +42,7 @@ class DataFactory(object):
'session_history.user_id', 'session_history.user_id',
'session_history.user', 'session_history.user',
'(CASE WHEN users.friendly_name IS NULL THEN user ELSE users.friendly_name END) as friendly_name', '(CASE WHEN users.friendly_name IS NULL THEN user ELSE users.friendly_name END) as friendly_name',
'platform',
'player', 'player',
'ip_address', 'ip_address',
'session_history_metadata.media_type', 'session_history_metadata.media_type',
@ -104,6 +105,12 @@ class DataFactory(object):
else: else:
watched_status = 0 watched_status = 0
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
row = {"reference_id": item["reference_id"], row = {"reference_id": item["reference_id"],
"id": item["id"], "id": item["id"],
"date": item["date"], "date": item["date"],
@ -114,6 +121,7 @@ class DataFactory(object):
"user_id": item["user_id"], "user_id": item["user_id"],
"user": item["user"], "user": item["user"],
"friendly_name": item["friendly_name"], "friendly_name": item["friendly_name"],
"platform": platform,
"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"],
@ -128,6 +136,7 @@ class DataFactory(object):
"thumb": thumb, "thumb": thumb,
"video_decision": item["video_decision"], "video_decision": item["video_decision"],
"audio_decision": item["audio_decision"], "audio_decision": item["audio_decision"],
"percent_complete": int(round(item['percent_complete'])),
"watched_status": watched_status, "watched_status": watched_status,
"group_count": item["group_count"], "group_count": item["group_count"],
"group_ids": item["group_ids"] "group_ids": item["group_ids"]
@ -151,7 +160,7 @@ class DataFactory(object):
home_stats = [] home_stats = []
for stat in stats_cards: for stat in stats_cards:
if 'top_tv' in stat: if stat == 'top_tv':
top_tv = [] top_tv = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
@ -197,7 +206,7 @@ class DataFactory(object):
'stat_type': sort_type, 'stat_type': sort_type,
'rows': top_tv}) 'rows': top_tv})
elif 'popular_tv' in stat: elif stat == 'popular_tv':
popular_tv = [] popular_tv = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
@ -243,7 +252,7 @@ class DataFactory(object):
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'rows': popular_tv}) 'rows': popular_tv})
elif 'top_movies' in stat: elif stat == 'top_movies':
top_movies = [] top_movies = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
@ -289,7 +298,7 @@ class DataFactory(object):
'stat_type': sort_type, 'stat_type': sort_type,
'rows': top_movies}) 'rows': top_movies})
elif 'popular_movies' in stat: elif stat == 'popular_movies':
popular_movies = [] popular_movies = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
@ -335,7 +344,7 @@ class DataFactory(object):
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'rows': popular_movies}) 'rows': popular_movies})
elif 'top_music' in stat: elif stat == 'top_music':
top_music = [] top_music = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
@ -381,7 +390,7 @@ class DataFactory(object):
'stat_type': sort_type, 'stat_type': sort_type,
'rows': top_music}) 'rows': top_music})
elif 'popular_music' in stat: elif stat == 'popular_music':
popular_music = [] popular_music = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
@ -427,7 +436,7 @@ class DataFactory(object):
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'rows': popular_music}) 'rows': popular_music})
elif 'top_users' in stat: elif stat == 'top_users':
top_users = [] top_users = []
try: try:
query = 'SELECT session_history.user, ' \ query = 'SELECT session_history.user, ' \
@ -480,7 +489,7 @@ class DataFactory(object):
'stat_type': sort_type, 'stat_type': sort_type,
'rows': top_users}) 'rows': top_users})
elif 'top_platforms' in stat: elif stat == 'top_platforms':
top_platform = [] top_platform = []
try: try:
@ -528,7 +537,7 @@ class DataFactory(object):
'stat_type': sort_type, 'stat_type': sort_type,
'rows': top_platform}) 'rows': top_platform})
elif 'last_watched' in stat: elif stat == 'last_watched':
last_watched = [] last_watched = []
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \

View file

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
@ -14,12 +17,11 @@
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers from plexpy import logger, helpers
from httplib import HTTPSConnection from httplib import HTTPSConnection
from httplib import HTTPConnection from httplib import HTTPConnection
import ssl import ssl
class HTTPHandler(object): class HTTPHandler(object):
""" """
Retrieve data from Plex Server Retrieve data from Plex Server
@ -88,6 +90,7 @@ class HTTPHandler(object):
return None return None
if request_status == 200: if request_status == 200:
try:
if output_format == 'dict': if output_format == 'dict':
output = helpers.convert_xml_to_dict(request_content) output = helpers.convert_xml_to_dict(request_content)
elif output_format == 'json': elif output_format == 'json':
@ -101,6 +104,11 @@ class HTTPHandler(object):
return output, content_type return output, content_type
return output return output
except Exception as e:
logger.warn(u"Failed format response from uri %s to %s error %s" % (uri, output_format, e))
return None
else: else:
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status)) logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status))
return None return None

View file

@ -467,8 +467,9 @@ class PROWL(object):
{'label': 'Priority', {'label': 'Priority',
'value': self.priority, 'value': self.priority,
'name': 'prowl_priority', 'name': 'prowl_priority',
'description': 'Set the priority (-2,-1,0,1 or 2).', 'description': 'Set the priority.',
'input_type': 'number' 'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
} }
] ]
@ -695,8 +696,9 @@ class NMA(object):
{'label': 'Priority', {'label': 'Priority',
'value': plexpy.CONFIG.NMA_PRIORITY, 'value': plexpy.CONFIG.NMA_PRIORITY,
'name': 'nma_priority', 'name': 'nma_priority',
'description': 'Set the priority (-2,-1,0,1 or 2).', 'description': 'Set the priority.',
'input_type': 'number' 'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
} }
] ]
@ -845,6 +847,7 @@ class PUSHOVER(object):
self.enabled = plexpy.CONFIG.PUSHOVER_ENABLED self.enabled = plexpy.CONFIG.PUSHOVER_ENABLED
self.keys = plexpy.CONFIG.PUSHOVER_KEYS self.keys = plexpy.CONFIG.PUSHOVER_KEYS
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
self.sound = plexpy.CONFIG.PUSHOVER_SOUND
self.on_play = plexpy.CONFIG.PUSHOVER_ON_PLAY self.on_play = plexpy.CONFIG.PUSHOVER_ON_PLAY
self.on_stop = plexpy.CONFIG.PUSHOVER_ON_STOP self.on_stop = plexpy.CONFIG.PUSHOVER_ON_STOP
self.on_watched = plexpy.CONFIG.PUSHOVER_ON_WATCHED self.on_watched = plexpy.CONFIG.PUSHOVER_ON_WATCHED
@ -867,6 +870,7 @@ class PUSHOVER(object):
'user': plexpy.CONFIG.PUSHOVER_KEYS, 'user': plexpy.CONFIG.PUSHOVER_KEYS,
'title': event, 'title': event,
'message': message.encode("utf-8"), 'message': message.encode("utf-8"),
'sound': plexpy.CONFIG.PUSHOVER_SOUND,
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY} 'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
http_handler.request("POST", http_handler.request("POST",
@ -893,30 +897,57 @@ class PUSHOVER(object):
#For uniformity reasons not removed #For uniformity reasons not removed
return return
def test(self, keys, priority): def test(self, keys, priority, sound):
self.enabled = True self.enabled = True
self.keys = keys self.keys = keys
self.priority = priority self.priority = priority
self.sound = sound
self.notify('Main Screen Activate', 'Test Message') self.notify('Main Screen Activate', 'Test Message')
def get_sounds(self):
http_handler = HTTPSConnection("api.pushover.net")
http_handler.request("GET", "/1/sounds.json?token=" + self.application_token)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
data = json.loads(response.read())
sounds = data.get('sounds', {})
sounds.update({'': ''})
return sounds
elif request_status >= 400 and request_status < 500:
logger.info(u"Unable to retrieve Pushover notification sounds list: %s" % response.reason)
return {'': ''}
else:
logger.info(u"Unable to retrieve Pushover notification sounds list.")
return {'': ''}
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Pushover API Key', config_option = [{'label': 'Pushover User Key',
'value': self.keys, 'value': self.keys,
'name': 'pushover_keys', 'name': 'pushover_keys',
'description': 'Your Pushover API key.', 'description': 'Your Pushover user key.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'Priority', {'label': 'Priority',
'value': self.priority, 'value': self.priority,
'name': 'pushover_priority', 'name': 'pushover_priority',
'description': 'Set the priority (-2,-1,0,1 or 2).', 'description': 'Set the priority.',
'input_type': 'number' 'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
},
{'label': 'Sound',
'value': self.sound,
'name': 'pushover_sound',
'description': 'Set the notification sound. Leave blank for the default sound.',
'input_type': 'select',
'select_options': self.get_sounds()
}, },
{'label': 'Pushover API Token', {'label': 'Pushover API Token',
'value': plexpy.CONFIG.PUSHOVER_APITOKEN, 'value': plexpy.CONFIG.PUSHOVER_APITOKEN,
'name': 'pushover_apitoken', 'name': 'pushover_apitoken',
'description': 'Your Pushover API toekn. Leave blank to use PlexPy default.', 'description': 'Your Pushover API token. Leave blank to use PlexPy default.',
'input_type': 'text' 'input_type': 'text'
} }
] ]
@ -1135,6 +1166,7 @@ class BOXCAR(object):
def __init__(self): def __init__(self):
self.url = 'https://new.boxcar.io/api/notifications' self.url = 'https://new.boxcar.io/api/notifications'
self.token = plexpy.CONFIG.BOXCAR_TOKEN self.token = plexpy.CONFIG.BOXCAR_TOKEN
self.sound = plexpy.CONFIG.BOXCAR_SOUND
self.on_play = plexpy.CONFIG.BOXCAR_ON_PLAY self.on_play = plexpy.CONFIG.BOXCAR_ON_PLAY
self.on_stop = plexpy.CONFIG.BOXCAR_ON_STOP self.on_stop = plexpy.CONFIG.BOXCAR_ON_STOP
self.on_watched = plexpy.CONFIG.BOXCAR_ON_WATCHED self.on_watched = plexpy.CONFIG.BOXCAR_ON_WATCHED
@ -1148,7 +1180,7 @@ class BOXCAR(object):
'user_credentials': plexpy.CONFIG.BOXCAR_TOKEN, 'user_credentials': plexpy.CONFIG.BOXCAR_TOKEN,
'notification[title]': title.encode('utf-8'), 'notification[title]': title.encode('utf-8'),
'notification[long_message]': message.encode('utf-8'), 'notification[long_message]': message.encode('utf-8'),
'notification[sound]': "done" 'notification[sound]': plexpy.CONFIG.BOXCAR_SOUND
}) })
req = urllib2.Request(self.url) req = urllib2.Request(self.url)
@ -1166,6 +1198,42 @@ class BOXCAR(object):
'name': 'boxcar_token', 'name': 'boxcar_token',
'description': 'Your Boxcar access token.', 'description': 'Your Boxcar access token.',
'input_type': 'text' 'input_type': 'text'
},
{'label': 'Sound',
'value': self.sound,
'name': 'boxcar_sound',
'description': 'Set the notification sound. Leave blank for the default sound.',
'input_type': 'select',
'select_options': {'': '',
'beep-crisp': 'Beep (Crisp)',
'beep-soft': 'Beep (Soft)',
'bell-modern': 'Bell (Modern)',
'bell-one-tone': 'Bell (One Tone)',
'bell-simple': 'Bell (Simple)',
'bell-triple': 'Bell (Triple)',
'bird-1': 'Bird (1)',
'bird-2': 'Bird (2)',
'boing': 'Boing',
'cash': 'Cash',
'clanging': 'Clanging',
'detonator-charge': 'Detonator Charge',
'digital-alarm': 'Digital Alarm',
'done': 'Done',
'echo': 'Echo',
'flourish': 'Flourish',
'harp': 'Harp',
'light': 'Light',
'magic-chime':'Magic Chime',
'magic-coin': 'Magic Coin',
'no-sound': 'No Sound',
'notifier-1': 'Notifier (1)',
'notifier-2': 'Notifier (2)',
'notifier-3': 'Notifier (3)',
'orchestral-long': 'Orchestral (Long)',
'orchestral-short': 'Orchestral (Short)',
'score': 'Score',
'success': 'Success',
'up': 'Up'}
} }
] ]

View file

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
@ -14,12 +17,14 @@
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers, users, http_handler, database from plexpy import logger, helpers, users, http_handler, database
import xmltodict
import json
from xml.dom import minidom from xml.dom import minidom
import base64 import base64
import plexpy import plexpy
def refresh_users(): def refresh_users():
logger.info("Requesting users list refresh...") logger.info("Requesting users list refresh...")
result = PlexTV().get_full_users_list() result = PlexTV().get_full_users_list()
@ -54,6 +59,7 @@ def refresh_users():
else: else:
logger.warn("Unable to refresh users list.") logger.warn("Unable to refresh users list.")
def get_real_pms_url(): def get_real_pms_url():
logger.info("Requesting URLs for server...") logger.info("Requesting URLs for server...")
@ -91,6 +97,7 @@ def get_real_pms_url():
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url) plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
plexpy.CONFIG.write() plexpy.CONFIG.write()
class PlexTV(object): class PlexTV(object):
""" """
Plex.tv authentication Plex.tv authentication
@ -133,7 +140,7 @@ class PlexTV(object):
if plextv_response: if plextv_response:
xml_head = plextv_response.getElementsByTagName('user') xml_head = plextv_response.getElementsByTagName('user')
if not xml_head: if not xml_head:
logger.warn("Error parsing XML for Plex.tv token: %s" % e) logger.warn("Error parsing XML for Plex.tv token")
return [] return []
auth_token = xml_head[0].getAttribute('authenticationToken') auth_token = xml_head[0].getAttribute('authenticationToken')
@ -402,3 +409,38 @@ class PlexTV(object):
server_urls.append(server_details) server_urls.append(server_details)
return server_urls return server_urls
def discover(self):
""" Query plex for all servers online. Returns the ones you own in a selectize format """
result = self.get_plextv_resources(include_https=True, output_format='raw')
servers = xmltodict.parse(result, process_namespaces=True, attr_prefix='')
clean_servers = []
try:
if servers:
# Fix if its only one "device"
if int(servers['MediaContainer']['size']) == 1:
servers['MediaContainer']['Device'] = [servers['MediaContainer']['Device']]
for server in servers['MediaContainer']['Device']:
# Only grab servers online and own
if server.get('presence', None) == '1' and server.get('owned', None) == '1' and server.get('provides', None) == 'server':
# If someone only has one connection..
if isinstance(server['Connection'], dict):
server['Connection'] = [server['Connection']]
for s in server['Connection']:
# to avoid circular ref
d = {}
d.update(s)
d.update(server)
d['label'] = d['name']
d['value'] = d['address']
del d['Connection']
clean_servers.append(d)
except Exception as e:
logger.warn('Failed to get servers from plex %s' % e)
return clean_servers
return json.dumps(clean_servers, indent=4)

View file

@ -32,7 +32,8 @@ class Users(object):
'MAX(session_history.started) as last_seen', 'MAX(session_history.started) as last_seen',
'session_history.ip_address as ip_address', 'session_history.ip_address as ip_address',
'COUNT(session_history.id) as plays', 'COUNT(session_history.id) as plays',
'session_history.player as platform', 'session_history.platform as platform',
'session_history.player as player',
'session_history_metadata.full_title as last_watched', 'session_history_metadata.full_title as last_watched',
'session_history_metadata.thumb', 'session_history_metadata.thumb',
'session_history_metadata.parent_thumb', 'session_history_metadata.parent_thumb',
@ -83,12 +84,19 @@ class Users(object):
else: else:
user_thumb = item['user_thumb'] user_thumb = item['user_thumb']
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
row = {"id": item['id'], row = {"id": item['id'],
"plays": item['plays'], "plays": item['plays'],
"last_seen": item['last_seen'], "last_seen": item['last_seen'],
"friendly_name": item['friendly_name'], "friendly_name": item['friendly_name'],
"ip_address": item['ip_address'], "ip_address": item['ip_address'],
"platform": item['platform'], "platform": platform,
"player": item['player'],
"last_watched": item['last_watched'], "last_watched": item['last_watched'],
"thumb": thumb, "thumb": thumb,
"media_type": item['media_type'], "media_type": item['media_type'],
@ -121,7 +129,8 @@ class Users(object):
'session_history.started as last_seen', 'session_history.started as last_seen',
'session_history.ip_address as ip_address', 'session_history.ip_address as ip_address',
'COUNT(session_history.id) as play_count', 'COUNT(session_history.id) as play_count',
'session_history.player as platform', 'session_history.platform as platform',
'session_history.player as player',
'session_history_metadata.full_title as last_watched', 'session_history_metadata.full_title as last_watched',
'session_history_metadata.thumb', 'session_history_metadata.thumb',
'session_history_metadata.parent_thumb', 'session_history_metadata.parent_thumb',
@ -169,11 +178,18 @@ class Users(object):
else: else:
thumb = item["thumb"] thumb = item["thumb"]
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
row = {"id": item['id'], row = {"id": item['id'],
"last_seen": item['last_seen'], "last_seen": item['last_seen'],
"ip_address": item['ip_address'], "ip_address": item['ip_address'],
"play_count": item['play_count'], "play_count": item['play_count'],
"platform": item['platform'], "platform": platform,
"player": item['player'],
"last_watched": item['last_watched'], "last_watched": item['last_watched'],
"thumb": thumb, "thumb": thumb,
"media_type": item['media_type'], "media_type": item['media_type'],
@ -490,10 +506,10 @@ class Users(object):
return user_watch_time_stats return user_watch_time_stats
def get_user_platform_stats(self, user=None, user_id=None): def get_user_player_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
platform_stats = [] player_stats = []
result_id = 0 result_id = 0
try: try:
@ -522,12 +538,12 @@ class Users(object):
'Mystery 5': 'Xbox 360'} 'Mystery 5': 'Xbox 360'}
platform_type = platform_names.get(item[2], item[2]) platform_type = platform_names.get(item[2], item[2])
row = {'platform_name': item[0], row = {'player_name': item[0],
'platform_type': platform_type, 'platform_type': platform_type,
'total_plays': item[1], 'total_plays': item[1],
'result_id': result_id 'result_id': result_id
} }
platform_stats.append(row) player_stats.append(row)
result_id += 1 result_id += 1
return platform_stats return player_stats

View file

@ -1,2 +1,2 @@
PLEXPY_VERSION = "master" PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.2.1" PLEXPY_RELEASE_VERSION = "1.2.2"

View file

@ -1,4 +1,7 @@
# This file is part of PlexPy. #!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -97,8 +100,7 @@ class WebInterface(object):
} }
# The setup wizard just refreshes the page on submit so we must redirect to home if config set. # The setup wizard just refreshes the page on submit so we must redirect to home if config set.
# Also redirecting to home if a PMS token already exists - will remove this in future. if plexpy.CONFIG.FIRST_RUN_COMPLETE:
if plexpy.CONFIG.FIRST_RUN_COMPLETE or plexpy.CONFIG.PMS_TOKEN:
plexpy.initialize_scheduler() plexpy.initialize_scheduler()
raise cherrypy.HTTPRedirect("home") raise cherrypy.HTTPRedirect("home")
else: else:
@ -587,6 +589,9 @@ class WebInterface(object):
if 'reference_id' in kwargs: if 'reference_id' in kwargs:
reference_id = kwargs.get('reference_id', "") reference_id = kwargs.get('reference_id', "")
custom_where = [['session_history.reference_id', reference_id]] custom_where = [['session_history.reference_id', reference_id]]
if 'media_type' in kwargs:
media_type = kwargs.get('media_type', "")
custom_where = [['session_history_metadata.media_type', media_type]]
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping, watched_percent=watched_percent) history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping, watched_percent=watched_percent)
@ -771,6 +776,12 @@ class WebInterface(object):
if source == 'history': if source == 'history':
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
metadata = data_factory.get_metadata_details(row_id=item_id) metadata = data_factory.get_metadata_details(row_id=item_id)
elif item_id == 'movie':
metadata = {'type': 'library', 'library': 'movie', 'media_type': 'movie', 'title': 'Movies'}
elif item_id == 'show':
metadata = {'type': 'library', 'library': 'show', 'media_type': 'episode', 'title': 'TV Shows'}
elif item_id == 'artist':
metadata = {'type': 'library', 'library': 'artist', 'media_type': 'track', 'title': 'Music'}
else: else:
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_metadata_details(rating_key=item_id) result = pms_connect.get_metadata_details(rating_key=item_id)
@ -813,17 +824,17 @@ class WebInterface(object):
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose @cherrypy.expose
def get_user_platform_stats(self, user=None, user_id=None, **kwargs): def get_user_player_stats(self, user=None, user_id=None, **kwargs):
user_data = users.Users() user_data = users.Users()
result = user_data.get_user_platform_stats(user_id=user_id, user=user) result = user_data.get_user_player_stats(user_id=user_id, user=user)
if result: if result:
return serve_template(templatename="user_platform_stats.html", data=result, return serve_template(templatename="user_player_stats.html", data=result,
title="Platform Stats") title="Player Stats")
else: else:
logger.warn('Unable to retrieve data.') logger.warn('Unable to retrieve data.')
return serve_template(templatename="user_platform_stats.html", data=None, title="Platform Stats") return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats")
@cherrypy.expose @cherrypy.expose
def get_item_children(self, rating_key='', **kwargs): def get_item_children(self, rating_key='', **kwargs):
@ -1294,7 +1305,7 @@ class WebInterface(object):
checkboxes = {'email_tls': checked(plexpy.CONFIG.EMAIL_TLS)} checkboxes = {'email_tls': checked(plexpy.CONFIG.EMAIL_TLS)}
return serve_template(templatename="notification_config.html", title="Notification Configuration", return serve_template(templatename="notification_config.html", title="Notification Configuration",
data=config, checkboxes=checkboxes) config_id=config_id, data=config, checkboxes=checkboxes)
@cherrypy.expose @cherrypy.expose
def get_notification_agent_triggers(self, config_id, **kwargs): def get_notification_agent_triggers(self, config_id, **kwargs):
@ -1397,7 +1408,6 @@ class WebInterface(object):
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'}) return json.dumps({'message': 'no data received'})
# test code # test code
@cherrypy.expose @cherrypy.expose
def get_new_rating_keys(self, rating_key='', media_type='', **kwargs): def get_new_rating_keys(self, rating_key='', media_type='', **kwargs):
@ -1442,3 +1452,19 @@ class WebInterface(object):
return json.dumps(result) return json.dumps(result)
else: else:
logger.warn('Unable to retrieve data.') logger.warn('Unable to retrieve data.')
@cherrypy.expose
def discover(self, token=''):
"""
Returns the servers that you own as a
list of dicts (formatted for selectize)
"""
# Need to set token so result dont return http 401
plexpy.CONFIG.__setattr__('PMS_TOKEN', token)
plexpy.CONFIG.write()
result = plextv.PlexTV()
servers = result.discover()
if servers:
cherrypy.response.headers['Content-type'] = 'application/json'
return servers