Compare commits

...

21 Commits

Author SHA1 Message Date
JonnyWong16
49fb4540a2 Merge branch 'dev' 2016-02-03 20:55:10 -08:00
JonnyWong16
e2120393a2 v1.3.6 2016-02-03 20:54:28 -08:00
JonnyWong16
0b301fff3f Fix regression where duration not reported as min 2016-02-03 09:32:32 -08:00
JonnyWong16
eeb351e991 Update readme 2016-02-02 22:34:46 -08:00
JonnyWong16
1095e29b4d Fix FreeBSD and FreeNAS init scripts daemonizing 2016-02-02 21:23:48 -08:00
JonnyWong16
be058eaff7 Merge branch 'dev' 2016-02-02 21:13:34 -08:00
JonnyWong16
f409dda2ef v1.3.5 2016-02-02 21:12:53 -08:00
JonnyWong16
f409cdda8f Merge pull request #502 from JonnyWong16/startup-tasks-after-daemonizing
Run startup tasks after daemonizing
2016-02-02 21:03:52 -08:00
JonnyWong16
9cd6396c35 Add method to delete duplicate libraries 2016-02-02 20:54:34 -08:00
JonnyWong16
ee754ea533 Remove trailing slash from Facebook redirect URI 2016-02-02 20:38:16 -08:00
JonnyWong16
36de20dd75 Fix getting new pms_identifier for server only 2016-02-02 20:33:47 -08:00
JonnyWong16
a957e8eb4f Clean up time formats for server notifications 2016-02-02 20:33:08 -08:00
JonnyWong16
14a90d84ec Add {stream_time}, {remaining_time}, and {progress_time} to notification options 2016-01-31 16:15:06 -08:00
JonnyWong16
fae9bc618a Initialize PlexPy after daemonizing 2016-01-31 15:13:35 -08:00
JonnyWong16
3248e6500e Clean up build_notify_text
* session is now a dict, so no need for "default values"
2016-01-31 13:34:51 -08:00
JonnyWong16
c17bf79d79 Fix server verification for unpublished servers 2016-01-31 11:32:44 -08:00
JonnyWong16
1ff1270bfa Clean up powershell for scripts 2016-01-30 16:18:45 -08:00
JonnyWong16
b1a2cf33d8 Merge pull request #498 from Hellowlol/ps1
add support for powershell
2016-01-30 15:54:34 -08:00
Hellowlol
b2292e98c1 add support for powershell 2016-01-31 00:18:58 +01:00
JonnyWong16
4d156a8911 Allow expanding of media info table when missing added at date 2016-01-30 00:48:51 -08:00
JonnyWong16
7193b6518b Fix removing unique constraints from database 2016-01-30 00:40:06 -08:00
19 changed files with 367 additions and 320 deletions

View File

@@ -1,5 +1,24 @@
# Changelog
## v1.3.6 (2016-02-03)
* Fix: Regression where {duration} not reported in minutes.
* Fix: Proper daemonizing in FreeBSD and FreeNAS init scripts.
* Change: Update readme documentation.
## v1.3.5 (2016-02-02)
* Fix: Removing unique constraints from database.
* Fix: Unable to expand media info table when missing "Added At" date.
* Fix: Server verification for unpublished servers.
* Fix: Updating PMS identifier for server change.
* Add: {stream_time}, {remaining_time}, and {progress_time} to notification options.
* Add: Powershell script support. (Thanks @Hellowlol)
* Add: Method to delete duplicate libraries.
* Change: Daemonize before running start up tasks.
## v1.3.4 (2016-01-29)
* Fix: Activity checker not starting with library update (history not logging).
@@ -8,10 +27,10 @@
* Fix: Libraries and Users lists not refreshing.
* Fix: Server verification in settings.
* Fix: Empty libraries not added to database.
* Add: Unique identifier to notification options.
* Remove: Media type toggles for recently added notifications.
* Add: Unique identifiers to notification options.
* Remove: Requirement of media type toggles for recently added notifications.
* Remove: Built in Twitter key and secret.
* Remove: Unnecessary quoting of script arguments.
* Change: Unnecessary quoting of script arguments.
* Change: Facebook notification instructions.

View File

@@ -153,12 +153,12 @@ def main():
# Put the database in the DATA_DIR
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db')
# Read config and start logging
plexpy.initialize(config_file)
if plexpy.DAEMON:
plexpy.daemonize()
# Read config and start logging
plexpy.initialize(config_file)
# Force the http port if neccessary
if args.port:
http_port = args.port

166
README.md
View File

@@ -1,121 +1,75 @@
#PlexPy
# PlexPy
[![Join the chat at https://gitter.im/drzoidberg33/plexpy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/drzoidberg33/plexpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
A python based web application for monitoring, analytics and notifications for Plex Media Server (www.plex.tv).
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).
This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).
* PlexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
* PlexPy [forum thread](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program)
## Features
* Responsive web design viewable on desktop, tablet and mobile web browsers.
* Themed to complement Plex/Web.
* Easy configuration setup (no separate web server required).
* Monitor current Plex Media Server activity.
* Fully customizable notifications for stream activity and recently added media.
* Top statistics on home page with configurable duration and measurement metric.
* Global watching history with search/filtering & dynamic column sorting.
* Full user list with general information and comparison stats.
* Individual user information including devices IP addresses.
* Complete library statistics and media file information.
* Rich analytics presented using Highcharts graphing.
* Beautiful content information pages.
* Full sync list data on all users syncing items from your library.
* And many more!!
###Support
-----------
* PlexPy Wiki: https://github.com/drzoidberg33/plexpy/wiki
## Installation and Support
###Features
-----------
* Responsive web design viewable on desktop, tablet and mobile web browsers
* Themed to complement Plex/Web
* Easy configuration setup via html form
* Current Plex Media Server viewing activity including:
* number of current users
* title
* progress
* platform
* user
* state (playing, paused, buffering, etc)
* stream type (direct, transcoded)
* video type & resolution
* audio type & channel count.
* Top statistics on home page with configurable duration and measurement metric:
* Most watched TV
* Most popular TV
* Most watched Movie
* Most popular Movie
* Most active user
* Most active platform
* Recently added media and how long ago it was added
* Global watching history with search/filtering & dynamic column sorting
* date
* user
* platform
* ip address
* title
* stream information details
* start time
* paused duration length
* stop time
* duration length
* watched progress
* show/hide columns
* delete mode - allows deletion of specific history items
* Full user list with general information and comparison stats
* Individual user information
* username and gravatar (if available)
* daily, weekly, monthly, all time stats for play count and duration length
* individual platform stats for each user
* public ip address history with last seen date and geo tag location
* recently watched content
* watching history
* synced items
* assign users custom friendly names within PlexPy
* assign users custom avatar URL within PlexPy
* disable history logging per user
* disable notifications per user
* option to purge all history per user.
* Rich analytics presented using Highcharts graphing
* user-selectable time periods of 30, 90 or 365 days
* daily watch count and duration
* totals by day of week and hours of the day
* totals by top 10 platform
* totals by top 10 users
* detailed breakdown by transcode decision
* source and stream resolutions
* transcode decision counts by user and platform
* total monthly counts
* Content information pages
* movies (includes watching history)
* tv shows (includes watching history)
* tv seasons
* tv episodes (includes watching history)
* Full sync list data on all users syncing items from your library
## Installation and Notes
* [Installation page](../../wiki/Installation) shows you how to install PlexPy.
* [Usage guide](../../wiki/Usage-guide) introduces you to PlexPy.
* [Troubleshooting page](../../wiki/TroubleShooting) in the wiki can help you with common problems.
**Issues** can be reported on the GitHub issue tracker considering these rules:
1. Analyze your log, you just might find the solution yourself!
2. You read the wiki and searched existing issues, but this is not solving your problem.
3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
**Feature requests** can be reported on the GitHub issue tracker too:
1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'.
2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one.
If you **comply with these rules** you can [post your request/issue](http://github.com/drzoidberg33/plexpy/issues).
* [Installation Guides](https://github.com/drzoidberg33/plexpy/wiki/Installation) shows you how to install PlexPy.
* [FAQs](https://github.com/drzoidberg33/plexpy/wiki/Frequently-Asked-Questions-(FAQ)) in the wiki can help you with common problems.
**Support** the project by implementing new features, solving support tickets and provide bug fixes.
## Issues
##### Many issues can simply be solved by:
- Making sure you update to the latest version.
- Turning your device off and on again.
- Analyzing your logs, you just might find the solution yourself!
- Using the **search** function to see if this issue has already been reported/solved.
- Checking the [Wiki](https://github.com/drzoidberg33/plexpy/wiki) for
[ [Installation] ](https://github.com/drzoidberg33/plexpy/wiki/Installation) and
[ [FAQs] ](https://github.com/drzoidberg33/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
- For basic questions try asking on [Gitter](https://gitter.im/drzoidberg33/plexpy) or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
##### If nothing has worked:
1. Open a new issue on the GitHub [issue tracker](http://github.com/drzoidberg33/plexpy/issues).
2. Provide a clear title to easily help identify your problem.
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
4. Make sure you provide the following information:
- [ ] Branch
- [ ] Version/Commit hash
- [ ] Your operating system and python version
- [ ] What you did?
- [ ] What happened?
- [ ] What you expected?
- [ ] How can we reproduce your issue?
- [ ] What are your (relevant) settings?
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
## Feature Requests
1. Search for similar existing 'issues', feature requests can be recognized by the blue `enhancement` label.
2. If a similar request exists, post a comment (+1, or add a new idea to the existing request).
3. If no similar requests exist, you can create a new one.
4. Provide a clear title to easily identify the feature request.
5. Tag your feature request with `[Feature Request]` so it can be identified easily.
## License
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.

View File

@@ -34,9 +34,12 @@ media_info_table_options = {
"targets": [0],
"data": "added_at",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
if (rowData) {
var expand_details = '';
var date = moment(cellData, "X").format(date_format);
var date = '';
if (cellData !== null && cellData !== '') {
date = moment(cellData, "X").format(date_format);
}
if (rowData['media_type'] === 'show') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Seasons"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');

View File

@@ -181,6 +181,10 @@ from plexpy import helpers
});
$('#facebookStep1').click(function () {
// Remove trailing '/' from Facebook redirect URI
if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) {
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
}
doAjaxCall('set_notification_config', $(this), 'tabs', true);
$.get('facebookStep1', function (data) { window.open(data); })
.done(function () { showMsg('<i class="fa fa-check"></i> Confirm Authorization. Check pop-up blocker if no response.', false, true, 3000); });

View File

@@ -1093,11 +1093,11 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
</tr>
<tr>
<td><strong>{datestamp}</strong></td>
<td>The date the notification was triggered.</td>
<td>The date (in date format) the notification was triggered.</td>
</tr>
<tr>
<td><strong>{timestamp}</strong></td>
<td>The time the notification was triggered.</td>
<td>The time (in time format) the notification was triggered.</td>
</tr>
</tbody>
</table>
@@ -1134,14 +1134,26 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
<td><strong>{stream_duration}</strong></td>
<td>The stream duration (in minutes) for the item.</td>
</tr>
<tr>
<td><strong>{stream_time}</strong></td>
<td>The stream duration (in time format) for the item.</td>
</tr>
<tr>
<td><strong>{remaining_duration}</strong></td>
<td>The remaining duration (in minutes) for the item.</td>
</tr>
<tr>
<td><strong>{progress}</strong></td>
<td><strong>{remaining_time}</strong></td>
<td>The remaining duration (in time format) for the item.</td>
</tr>
<tr>
<td><strong>{progress_duration}</strong></td>
<td>The last reported offset (in minutes) for the item.</td>
</tr>
<tr>
<td><strong>{progress_time}</strong></td>
<td>The last reported offset (in time format) for the item.</td>
</tr>
<tr>
<td><strong>{progress_percent}</strong></td>
<td>The last reported progress percent for the item.</td>
@@ -1222,6 +1234,10 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
<td><strong>{transcode_audio_channels}</strong></td>
<td>The audio channels of the transcoded media.</td>
</tr>
<tr>
<td><strong>{session_key}</strong></td>
<td>The unique identifier for the session.</td>
</tr>
<tr>
<td><strong>{user_id}</strong></td>
<td>The unique identifier for the user.</td>
@@ -1569,11 +1585,11 @@ $(document).ready(function() {
});
function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val()
var pms_port = $("#pms_port").val()
var pms_identifier = $("#pms_identifier").val()
var pms_ssl = $("#pms_ssl").val()
var pms_is_remote = $("#pms_is_remote").val()
var pms_ip = $("#pms_ip").val();
var pms_port = $("#pms_port").val();
var pms_identifier = $("#pms_identifier").val();
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>');
$('#pms-verify').fadeIn('fast');
@@ -1582,15 +1598,16 @@ $(document).ready(function() {
data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
cache: true,
async: true,
timeout: 5000,
timeout: 10000,
error: function(jqXHR, textStatus, errorThrown) {
$("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error");
},
success: function (xml) {
if ($(xml).find('MediaContainer').attr('machineIdentifier')) {
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier'));
success: function (json) {
var machine_identifier = json;
if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
$("#pms-verify").html('<i class="fa fa-check"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").removeClass("has-error");

View File

@@ -393,9 +393,10 @@ from plexpy import common
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast');
},
success: function (xml) {
if ($(xml).find('MediaContainer').attr('machineIdentifier')) {
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier'));
success: function (json) {
var machine_identifier = json;
if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
$('#pms-verify-status').fadeIn('fast');
pms_verified = true;

View File

@@ -38,7 +38,7 @@ status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="/usr/sbin/daemon"
command_args="-f -p ${plexpy_pid} python ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
command_args="python2 ${plexpy_dir}/PlexPy.py --daemon --pidfile ${plexpy_pid} --quiet --nolaunch"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then

View File

@@ -38,7 +38,7 @@ status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="/usr/sbin/daemon"
command_args="-f -p ${plexpy_pid} python2 ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
command_args="python2 ${plexpy_dir}/PlexPy.py --daemon --pidfile ${plexpy_pid} --quiet --nolaunch"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then

View File

@@ -713,8 +713,8 @@ def dbcheck():
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
try:
result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_library_sections_1")')
if result and 'server_id' not in [row[2] for row in result]:
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()
if 'section_id INTEGER UNIQUE' in result[0]:
logger.debug(u"Altering database. Removing unique constraint on section_id from library_sections table.")
c_db.execute(
'CREATE TABLE library_sections_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '
@@ -760,8 +760,8 @@ def dbcheck():
# Upgrade users table from earlier versions (remove UNIQUE constraint on username)
try:
result = c_db.execute('PRAGMA index_xinfo("sqlite_autoindex_users_2")')
if result and 'username' in [row[2] for row in result]:
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="users"').fetchone()
if 'username TEXT NOT NULL UNIQUE' in result[0]:
logger.debug(u"Altering database. Removing unique constraint on username from users table.")
c_db.execute(
'CREATE TABLE users_temp (id INTEGER PRIMARY KEY AUTOINCREMENT, '

View File

@@ -512,6 +512,7 @@ class Config(object):
self.MOVIE_LOGGING_ENABLE = 0
self.TV_LOGGING_ENABLE = 0
self.CONFIG_VERSION = '1'
if self.CONFIG_VERSION == '1':
# Change home_stats_cards to list
if self.HOME_STATS_CARDS:
@@ -525,4 +526,20 @@ class Config(object):
if 'library_statistics' in home_library_cards:
home_library_cards.remove('library_statistics')
self.HOME_LIBRARY_CARDS = home_library_cards
self.CONFIG_VERSION = '2'
self.CONFIG_VERSION = '2'
if self.CONFIG_VERSION == '2':
self.NOTIFY_ON_START_SUBJECT_TEXT = self.NOTIFY_ON_START_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_START_BODY_TEXT = self.NOTIFY_ON_START_BODY_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_STOP_SUBJECT_TEXT = self.NOTIFY_ON_STOP_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_STOP_BODY_TEXT = self.NOTIFY_ON_STOP_BODY_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_PAUSE_SUBJECT_TEXT = self.NOTIFY_ON_PAUSE_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_PAUSE_BODY_TEXT = self.NOTIFY_ON_PAUSE_BODY_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_RESUME_SUBJECT_TEXT = self.NOTIFY_ON_RESUME_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_RESUME_BODY_TEXT = self.NOTIFY_ON_RESUME_BODY_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_BUFFER_SUBJECT_TEXT = self.NOTIFY_ON_BUFFER_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_BUFFER_BODY_TEXT = self.NOTIFY_ON_BUFFER_BODY_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_WATCHED_SUBJECT_TEXT = self.NOTIFY_ON_WATCHED_SUBJECT_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_ON_WATCHED_BODY_TEXT = self.NOTIFY_ON_WATCHED_BODY_TEXT.replace('{progress}','{progress_duration}')
self.NOTIFY_SCRIPTS_ARGS_TEXT = self.NOTIFY_SCRIPTS_ARGS_TEXT.replace('{progress}','{progress_duration}')
self.CONFIG_VERSION = '3'

View File

@@ -135,6 +135,15 @@ def convert_seconds(s):
return minutes
def convert_seconds_to_minutes(s):
if str(s).isdigit():
minutes = round(float(s) / 60, 0)
return math.trunc(minutes)
return 0
def today():
today = datetime.date.today()
@@ -369,14 +378,14 @@ def create_https_certificates(ssl_cert, ssl_key):
def cast_to_int(s):
try:
return int(s)
except ValueError:
return -1
except (ValueError, TypeError):
return 0
def cast_to_float(s):
try:
return float(s)
except ValueError:
return -1
except (ValueError, TypeError):
return 0
def convert_xml_to_json(xml):
o = xmltodict.parse(xml)

View File

@@ -44,7 +44,8 @@ class HTTPHandler(object):
headers=None,
output_format='raw',
return_type=False,
no_token=False):
no_token=False,
timeout=20):
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
@@ -56,12 +57,12 @@ class HTTPHandler(object):
if proto.upper() == 'HTTPS':
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
context = ssl._create_unverified_context()
handler = HTTPSConnection(host=self.host, port=self.port, timeout=20, context=context)
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout, context=context)
logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
else:
handler = HTTPSConnection(host=self.host, port=self.port, timeout=20)
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout)
else:
handler = HTTPConnection(host=self.host, port=self.port, timeout=20)
handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout)
token_string = ''
if not no_token:

View File

@@ -879,3 +879,21 @@ class Libraries(object):
return 'Unable to delete media info table cache, section_id not valid.'
except Exception as e:
logger.warn(u"PlexPy Libraries :: Unable to delete media info table cache: %s." % e)
def delete_duplicate_libraries(self):
from plexpy import plextv
monitor_db = database.MonitorDatabase()
# Refresh the PMS_URL to make sure the server_id is updated
plextv.get_real_pms_url()
server_id = plexpy.CONFIG.PMS_IDENTIFIER
try:
logger.debug(u"PlexPy Libraries :: Deleting libraries where server_id does not match %s." % server_id)
monitor_db.action('DELETE FROM library_sections WHERE server_id != ?', [server_id])
return 'Deleted duplicate libraries from the database.'
except Exception as e:
logger.warn(u"PlexPy Libraries :: Unable to delete duplicate libraries: %s." % e)

View File

@@ -340,6 +340,11 @@ def set_notify_state(session, state, agent_info):
def build_notify_text(session=None, timeline=None, state=None):
# Get time formats
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','').replace('a','').replace('A','')
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
@@ -349,7 +354,7 @@ def build_notify_text(session=None, timeline=None, state=None):
if server_times:
updated_at = server_times[0]['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
else:
logger.error(u"PlexPy NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
@@ -428,80 +433,30 @@ def build_notify_text(session=None, timeline=None, state=None):
else:
full_title = metadata['title']
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
# Default values
user = ''
platform = ''
player = ''
ip_address = 'N/A'
stream_duration = 0
view_offset = 0
container = ''
video_codec = ''
video_bitrate = ''
video_width = ''
video_height = ''
video_resolution = ''
video_framerate = ''
aspect_ratio = ''
audio_codec = ''
audio_channels = ''
transcode_decision = ''
video_decision = ''
audio_decision = ''
transcode_container = ''
transcode_video_codec = ''
transcode_video_width = ''
transcode_video_height = ''
transcode_audio_codec = ''
transcode_audio_channels = ''
user_id = ''
# Session values
if session:
# Generate a combined transcode decision value
video_decision = session['video_decision'].title()
audio_decision = session['audio_decision'].title()
if session is None:
session = {}
if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode':
transcode_decision = 'Transcode'
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
transcode_decision = 'Direct Stream'
else:
transcode_decision = 'Direct Play'
if state != 'play':
if session['paused_counter']:
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) -
helpers.cast_to_float(session['paused_counter'])) / 60)
else:
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60)
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
user = session['friendly_name']
platform = session['platform']
player = session['player']
ip_address = session['ip_address'] if session['ip_address'] else 'N/A'
container = session['container']
video_codec = session['video_codec']
video_bitrate = session['bitrate']
video_width = session['width']
video_height = session['height']
video_resolution = session['video_resolution']
video_framerate = session['video_framerate']
aspect_ratio = session['aspect_ratio']
audio_codec = session['audio_codec']
audio_channels = session['audio_channels']
transcode_container = session['transcode_container']
transcode_video_codec = session['transcode_video_codec']
transcode_video_width = session['transcode_width']
transcode_video_height = session['transcode_height']
transcode_audio_codec = session['transcode_audio_codec']
transcode_audio_channels = session['transcode_audio_channels']
user_id = session['user_id']
# Generate a combined transcode decision value
if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode':
transcode_decision = 'Transcode'
elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy':
transcode_decision = 'Direct Stream'
else:
transcode_decision = 'Direct Play'
if state != 'play':
stream_duration = helpers.convert_seconds_to_minutes(
time.time() -
helpers.cast_to_int(session.get('started', 0)) -
helpers.cast_to_int(session.get('paused_counter', 0)))
else:
stream_duration = 0
view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 0))
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
progress_percent = helpers.get_percent(view_offset, duration)
remaining_duration = duration - view_offset
# Fix metadata params for notify recently added grandparent
if state == 'created' and plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
@@ -517,41 +472,48 @@ def build_notify_text(session=None, timeline=None, state=None):
album_name = metadata['parent_title']
track_name = metadata['title']
available_params = {'server_name': server_name,
available_params = {# Global paramaters
'server_name': server_name,
'server_uptime': server_uptime,
'action': state,
'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')),
'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')),
'action': state.title(),
'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(time_format),
# Stream parameters
'streams': stream_count,
'user': user,
'platform': platform,
'player': player,
'ip_address': ip_address,
'media_type': metadata['media_type'],
'user': session.get('friendly_name',''),
'platform': session.get('platform',''),
'player': session.get('player',''),
'ip_address': session.get('ip_address','N/A'),
'stream_duration': stream_duration,
'remaining_duration': duration - view_offset,
'progress': view_offset,
'stream_time': arrow.get(stream_duration * 60).format(duration_format),
'remaining_duration': remaining_duration,
'remaining_time': arrow.get(remaining_duration * 60).format(duration_format),
'progress_duration': view_offset,
'progress_time': arrow.get(view_offset * 60).format(duration_format),
'progress_percent': progress_percent,
'container': container,
'video_codec': video_codec,
'video_bitrate': video_bitrate,
'video_width': video_width,
'video_height': video_height,
'video_resolution': video_resolution,
'video_framerate': video_framerate,
'aspect_ratio': aspect_ratio,
'audio_codec': audio_codec,
'audio_channels': audio_channels,
'container': session.get('container',''),
'video_codec': session.get('video_codec',''),
'video_bitrate': session.get('bitrate',''),
'video_width': session.get('width',''),
'video_height': session.get('height',''),
'video_resolution': session.get('video_resolution',''),
'video_framerate': session.get('video_framerate',''),
'aspect_ratio': session.get('aspect_ratio',''),
'audio_codec': session.get('audio_codec',''),
'audio_channels': session.get('audio_channels',''),
'transcode_decision': transcode_decision,
'video_decision': video_decision,
'audio_decision': audio_decision,
'transcode_container': transcode_container,
'transcode_video_codec': transcode_video_codec,
'transcode_video_width': transcode_video_width,
'transcode_video_height': transcode_video_height,
'transcode_audio_codec': transcode_audio_codec,
'transcode_audio_channels': transcode_audio_channels,
'user_id': user_id,
'video_decision': session.get('video_decision','').title(),
'audio_decision': session.get('audio_decision','').title(),
'transcode_container': session.get('transcode_container',''),
'transcode_video_codec': session.get('transcode_video_codec',''),
'transcode_video_width': session.get('transcode_width',''),
'transcode_video_height': session.get('transcode_height',''),
'transcode_audio_codec': session.get('transcode_audio_codec',''),
'transcode_audio_channels': session.get('transcode_audio_channels',''),
'session_key': session.get('session_key',''),
'user_id': session.get('user_id',''),
# Metadata parameters
'media_type': metadata['media_type'],
'title': full_title,
'library_name': metadata['library_name'],
'show_name': show_name,
@@ -598,9 +560,9 @@ def build_notify_text(session=None, timeline=None, state=None):
if state == 'play':
# Default body text
body_text = '%s (%s) is watching %s' % (session['friendly_name'],
session['player'],
full_title)
body_text = '%s (%s) started playing %s' % (session['friendly_name'],
session['player'],
full_title)
if on_start_subject and on_start_body:
try:
@@ -767,6 +729,10 @@ def build_notify_text(session=None, timeline=None, state=None):
def build_server_notify_text(state=None):
# Get time formats
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
@@ -776,7 +742,7 @@ def build_server_notify_text(state=None):
if server_times:
updated_at = server_times[0]['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
else:
logger.error(u"PlexPy NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
@@ -791,11 +757,12 @@ def build_server_notify_text(state=None):
on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT
script_args_text = plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT
available_params = {'server_name': server_name,
available_params = {# Global paramaters
'server_name': server_name,
'server_uptime': server_uptime,
'action': state,
'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')),
'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz',''))}
'action': state.title(),
'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(time_format)}
# Default text
subject_text = 'PlexPy (%s)' % server_name
@@ -907,4 +874,4 @@ def build_server_notify_text(state=None):
def strip_tag(data):
p = re.compile(r'<.*?>')
return p.sub('', data)
return p.sub('', data)

View File

@@ -34,7 +34,7 @@ from pynma import pynma
import gntp.notifier
import oauth2 as oauth
import pythontwitter as twitter
import pythonfacebook as facebook
import pythonfacebook as facebook
import plexpy
from plexpy import logger, helpers, request
@@ -58,7 +58,7 @@ AGENT_IDS = {"Growl": 0,
"Scripts": 15,
"Facebook": 16}
def available_notification_agents():
agents = [{'name': 'Growl',
'id': AGENT_IDS['Growl'],
@@ -1777,7 +1777,7 @@ class SLACK(object):
class Scripts(object):
def __init__(self, **kwargs):
self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.py', '.pyw', '.rb', '.sh')
self.script_exts = ('.bat', '.cmd', '.exe', '.php', '.pl', '.ps1', '.py', '.pyw', '.rb', '.sh')
def conf(self, options):
return cherrypy.config['config'].get('Scripts', options)
@@ -1807,7 +1807,7 @@ class Scripts(object):
return scripts
def notify(self, subject='', message='', notify_action='', script_args=[], *args, **kwargs):
def notify(self, subject='', message='', notify_action='', script_args=None, *args, **kwargs):
"""
Args:
subject(string, optional): Head text,
@@ -1817,7 +1817,10 @@ class Scripts(object):
"""
logger.debug(u"PlexPy Notifiers :: Trying to run notify script, action: %s, arguments: %s" %
(notify_action if notify_action else None, script_args if script_args else None))
if script_args is None:
script_args = []
if not plexpy.CONFIG.SCRIPTS_FOLDER:
return
@@ -1869,14 +1872,16 @@ class Scripts(object):
name, ext = os.path.splitext(script)
if ext == '.py':
prefix = 'python'
elif ext == '.pyw':
prefix = 'pythonw'
elif ext == '.php':
if ext == '.php':
prefix = 'php'
elif ext == '.pl':
prefix = 'perl'
elif ext == '.ps1':
prefix = 'powershell -executionPolicy bypass -file'
elif ext == '.py':
prefix = 'python'
elif ext == '.pyw':
prefix = 'pythonw'
elif ext == '.rb':
prefix = 'ruby'
else:
@@ -1886,7 +1891,7 @@ class Scripts(object):
script = script.encode(plexpy.SYS_ENCODING, 'ignore')
if prefix:
script = [prefix, script]
script = prefix.split() + [script]
else:
script = [script]
@@ -2025,7 +2030,7 @@ class Scripts(object):
return config_option
class FacebookNotifier(object):
def __init__(self):
@@ -2050,7 +2055,7 @@ class FacebookNotifier(object):
def _get_credentials(self, code):
logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook")
try:
# Request user access token
api = facebook.GraphAPI(version='2.5')
@@ -2059,19 +2064,19 @@ class FacebookNotifier(object):
app_id=self.app_id,
app_secret=self.app_secret)
access_token = response['access_token']
# Request extended user access token
api = facebook.GraphAPI(access_token=access_token, version='2.5')
response = api.extend_access_token(app_id=self.app_id,
app_secret=self.app_secret)
access_token = response['access_token']
plexpy.CONFIG.FACEBOOK_TOKEN = access_token
plexpy.CONFIG.write()
except Exception as e:
logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e)
return False
return True
def _post_facebook(self, message=None):

View File

@@ -383,7 +383,6 @@ class PlexTV(object):
return []
plextv_resources = self.get_plextv_resources(include_https=include_https)
server_urls = []
try:
xml_parse = minidom.parseString(plextv_resources)
@@ -400,36 +399,51 @@ class PlexTV(object):
logger.warn(u"PlexPy PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
return []
# Function to get all connections for a device
def get_connections(device):
conn = []
connections = device.getElementsByTagName('Connection')
for c in connections:
server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'),
"address": helpers.get_xml_attr(c, 'address'),
"port": helpers.get_xml_attr(c, 'port'),
"uri": helpers.get_xml_attr(c, 'uri'),
"local": helpers.get_xml_attr(c, 'local')
}
conn.append(server_details)
return conn
server_urls = []
# Try to match the device
for a in xml_head:
if helpers.get_xml_attr(a, 'clientIdentifier') == server_id:
connections = a.getElementsByTagName('Connection')
for connection in connections:
server_details = {"protocol": helpers.get_xml_attr(connection, 'protocol'),
"address": helpers.get_xml_attr(connection, 'address'),
"port": helpers.get_xml_attr(connection, 'port'),
"uri": helpers.get_xml_attr(connection, 'uri'),
"local": helpers.get_xml_attr(connection, 'local')
}
server_urls = get_connections(a)
break
# Else no device match found
if not server_urls:
# Try to match the PMS_IP and PMS_PORT
for a in xml_head:
if helpers.get_xml_attr(a, 'provides') == 'server':
connections = a.getElementsByTagName('Connection')
server_urls.append(server_details)
# Else try to match the PMS_IP and PMS_PORT
else:
connections = a.getElementsByTagName('Connection')
for connection in connections:
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
for connection in connections:
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
plexpy.CONFIG.write()
logger.info(u"PlexPy PlexTV :: PMS identifier changed from %s to %s." % \
(server_id, plexpy.CONFIG.PMS_IDENTIFIER))
server_urls = get_connections(a)
break
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
logger.info(u"PlexPy PlexTV :: PMS identifier changed from %s to %s." % \
(server_id, plexpy.CONFIG.PMS_IDENTIFIER))
server_details = {"protocol": helpers.get_xml_attr(connection, 'protocol'),
"address": helpers.get_xml_attr(connection, 'address'),
"port": helpers.get_xml_attr(connection, 'port'),
"uri": helpers.get_xml_attr(connection, 'uri'),
"local": helpers.get_xml_attr(connection, 'local')
}
if server_urls:
break
return server_urls

View File

@@ -1,2 +1,2 @@
PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.3.4"
PLEXPY_RELEASE_VERSION = "1.3.6"

View File

@@ -492,7 +492,20 @@ class WebInterface(object):
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})
else:
return json.dumps({'message': 'Cannot refresh library while getting file sizes.'})
return json.dumps({'message': 'Cannot refresh library while getting file sizes.'})
@cherrypy.expose
def delete_duplicate_libraries(self):
library_data = libraries.Libraries()
result = library_data.delete_duplicate_libraries()
if result:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': result})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'Unable to delete duplicate libraries from the database.'})
##### Users #####
@@ -1382,7 +1395,11 @@ class WebInterface(object):
@cherrypy.expose
def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs):
if not identifier:
from plexpy import http_handler
# Attempt to get the pms_identifier from plex.tv if the server is published
# Works for all PMS SSL settings
if not identifier and hostname and port:
plex_tv = plextv.PlexTV()
servers = plex_tv.discover()
@@ -1391,27 +1408,28 @@ class WebInterface(object):
identifier = server['clientIdentifier']
break
if identifier and hostname and port:
# Set PMS attributes to get the real PMS url
plexpy.CONFIG.__setattr__('PMS_IP', hostname)
plexpy.CONFIG.__setattr__('PMS_PORT', port)
plexpy.CONFIG.__setattr__('PMS_IDENTIFIER', identifier)
plexpy.CONFIG.__setattr__('PMS_SSL', ssl)
plexpy.CONFIG.__setattr__('PMS_IS_REMOTE', remote)
plexpy.CONFIG.write()
plextv.get_real_pms_url()
pms_connect = pmsconnect.PmsConnect()
request = pms_connect.get_local_server_identity()
if request:
cherrypy.response.headers['Content-type'] = 'application/xml'
return request
else:
logger.warn(u"Unable to retrieve data for get_server_id.")
return None
# Fallback to checking /identity endpoint is server is unpublished
# Cannot set SSL settings on the PMS if unpublished so 'http' is okay
if not identifier:
request_handler = http_handler.HTTPHandler(host=hostname,
port=port,
token=None)
uri = '/identity'
request = request_handler.make_request(uri=uri,
proto='http',
request_type='GET',
output_format='xml',
no_token=True,
timeout=10)
if request:
xml_head = request.getElementsByTagName('MediaContainer')[0]
identifier = xml_head.getAttribute('machineIdentifier')
if identifier:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps(identifier)
else:
logger.warn('Unable to retrieve the PMS identifier.')
return None
@cherrypy.expose