Merge branch 'dev'

This commit is contained in:
JonnyWong16 2016-02-02 21:13:34 -08:00
commit be058eaff7
16 changed files with 293 additions and 207 deletions

View file

@ -1,5 +1,17 @@
# Changelog # Changelog
## 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) ## v1.3.4 (2016-01-29)
* Fix: Activity checker not starting with library update (history not logging). * Fix: Activity checker not starting with library update (history not logging).
@ -8,10 +20,10 @@
* Fix: Libraries and Users lists not refreshing. * Fix: Libraries and Users lists not refreshing.
* Fix: Server verification in settings. * Fix: Server verification in settings.
* Fix: Empty libraries not added to database. * Fix: Empty libraries not added to database.
* Add: Unique identifier to notification options. * Add: Unique identifiers to notification options.
* Remove: Media type toggles for recently added notifications. * Remove: Requirement of media type toggles for recently added notifications.
* Remove: Built in Twitter key and secret. * Remove: Built in Twitter key and secret.
* Remove: Unnecessary quoting of script arguments. * Change: Unnecessary quoting of script arguments.
* Change: Facebook notification instructions. * Change: Facebook notification instructions.

View file

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

View file

@ -34,9 +34,12 @@ media_info_table_options = {
"targets": [0], "targets": [0],
"data": "added_at", "data": "added_at",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') { if (rowData) {
var expand_details = ''; 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') { 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>'; 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>'); $(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 () { $('#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); doAjaxCall('set_notification_config', $(this), 'tabs', true);
$.get('facebookStep1', function (data) { window.open(data); }) $.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); }); .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>
<tr> <tr>
<td><strong>{datestamp}</strong></td> <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>
<tr> <tr>
<td><strong>{timestamp}</strong></td> <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> </tr>
</tbody> </tbody>
</table> </table>
@ -1134,14 +1134,26 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
<td><strong>{stream_duration}</strong></td> <td><strong>{stream_duration}</strong></td>
<td>The stream duration (in minutes) for the item.</td> <td>The stream duration (in minutes) for the item.</td>
</tr> </tr>
<tr>
<td><strong>{stream_time}</strong></td>
<td>The stream duration (in time format) for the item.</td>
</tr>
<tr> <tr>
<td><strong>{remaining_duration}</strong></td> <td><strong>{remaining_duration}</strong></td>
<td>The remaining duration (in minutes) for the item.</td> <td>The remaining duration (in minutes) for the item.</td>
</tr> </tr>
<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> <td>The last reported offset (in minutes) for the item.</td>
</tr> </tr>
<tr>
<td><strong>{progress_time}</strong></td>
<td>The last reported offset (in time format) for the item.</td>
</tr>
<tr> <tr>
<td><strong>{progress_percent}</strong></td> <td><strong>{progress_percent}</strong></td>
<td>The last reported progress percent for the item.</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><strong>{transcode_audio_channels}</strong></td>
<td>The audio channels of the transcoded media.</td> <td>The audio channels of the transcoded media.</td>
</tr> </tr>
<tr>
<td><strong>{session_key}</strong></td>
<td>The unique identifier for the session.</td>
</tr>
<tr> <tr>
<td><strong>{user_id}</strong></td> <td><strong>{user_id}</strong></td>
<td>The unique identifier for the user.</td> <td>The unique identifier for the user.</td>
@ -1569,11 +1585,11 @@ $(document).ready(function() {
}); });
function verifyServer(_callback) { function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val() var pms_ip = $("#pms_ip").val();
var pms_port = $("#pms_port").val() var pms_port = $("#pms_port").val();
var pms_identifier = $("#pms_identifier").val() var pms_identifier = $("#pms_identifier").val();
var pms_ssl = $("#pms_ssl").val() var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
var pms_is_remote = $("#pms_is_remote").val() var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) { if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>'); $("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>');
$('#pms-verify').fadeIn('fast'); $('#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 }, data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
cache: true, cache: true,
async: true, async: true,
timeout: 5000, timeout: 10000,
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
$("#pms-verify").html('<i class="fa fa-close"></i>'); $("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast'); $('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error"); $("#pms-ip-group").addClass("has-error");
}, },
success: function (xml) { success: function (json) {
if ($(xml).find('MediaContainer').attr('machineIdentifier')) { var machine_identifier = json;
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier')); if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
$("#pms-verify").html('<i class="fa fa-check"></i>'); $("#pms-verify").html('<i class="fa fa-check"></i>');
$('#pms-verify').fadeIn('fast'); $('#pms-verify').fadeIn('fast');
$("#pms-ip-group").removeClass("has-error"); $("#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").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast'); $('#pms-verify-status').fadeIn('fast');
}, },
success: function (xml) { success: function (json) {
if ($(xml).find('MediaContainer').attr('machineIdentifier')) { var machine_identifier = json;
$("#pms_identifier").val($(xml).find('MediaContainer').attr('machineIdentifier')); if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!'); $("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
$('#pms-verify-status').fadeIn('fast'); $('#pms-verify-status').fadeIn('fast');
pms_verified = true; pms_verified = true;

View file

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

View file

@ -512,6 +512,7 @@ class Config(object):
self.MOVIE_LOGGING_ENABLE = 0 self.MOVIE_LOGGING_ENABLE = 0
self.TV_LOGGING_ENABLE = 0 self.TV_LOGGING_ENABLE = 0
self.CONFIG_VERSION = '1' self.CONFIG_VERSION = '1'
if self.CONFIG_VERSION == '1': if self.CONFIG_VERSION == '1':
# Change home_stats_cards to list # Change home_stats_cards to list
if self.HOME_STATS_CARDS: if self.HOME_STATS_CARDS:
@ -525,4 +526,20 @@ class Config(object):
if 'library_statistics' in home_library_cards: if 'library_statistics' in home_library_cards:
home_library_cards.remove('library_statistics') home_library_cards.remove('library_statistics')
self.HOME_LIBRARY_CARDS = home_library_cards 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 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(): def today():
today = datetime.date.today() today = datetime.date.today()

View file

@ -44,7 +44,8 @@ class HTTPHandler(object):
headers=None, headers=None,
output_format='raw', output_format='raw',
return_type=False, return_type=False,
no_token=False): no_token=False,
timeout=20):
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE'] valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
@ -56,12 +57,12 @@ class HTTPHandler(object):
if proto.upper() == 'HTTPS': if proto.upper() == 'HTTPS':
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'): if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
context = 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.") logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
else: else:
handler = HTTPSConnection(host=self.host, port=self.port, timeout=20) handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout)
else: else:
handler = HTTPConnection(host=self.host, port=self.port, timeout=20) handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout)
token_string = '' token_string = ''
if not no_token: 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.' return 'Unable to delete media info table cache, section_id not valid.'
except Exception as e: except Exception as e:
logger.warn(u"PlexPy Libraries :: Unable to delete media info table cache: %s." % 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): 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 # Get the server name
server_name = plexpy.CONFIG.PMS_NAME server_name = plexpy.CONFIG.PMS_NAME
@ -428,80 +433,30 @@ def build_notify_text(session=None, timeline=None, state=None):
else: else:
full_title = metadata['title'] 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 # Session values
if session: if session is None:
# Generate a combined transcode decision value session = {}
video_decision = session['video_decision'].title()
audio_decision = session['audio_decision'].title()
if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode': # Generate a combined transcode decision value
transcode_decision = 'Transcode' if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode':
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy': transcode_decision = 'Transcode'
transcode_decision = 'Direct Stream' elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy':
else: transcode_decision = 'Direct Stream'
transcode_decision = 'Direct Play' else:
transcode_decision = 'Direct Play'
if state != 'play':
if session['paused_counter']: if state != 'play':
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) - stream_duration = helpers.convert_seconds_to_minutes(
helpers.cast_to_float(session['paused_counter'])) / 60) time.time() -
else: helpers.cast_to_float(session.get('started', 0)) -
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60) helpers.cast_to_float(session.get('paused_counter', 0)))
else:
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset']) stream_duration = 0
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']
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) progress_percent = helpers.get_percent(view_offset, duration)
remaining_duration = duration - view_offset
# Fix metadata params for notify recently added grandparent # Fix metadata params for notify recently added grandparent
if state == 'created' and plexpy.CONFIG.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'] album_name = metadata['parent_title']
track_name = metadata['title'] track_name = metadata['title']
available_params = {'server_name': server_name, available_params = {# Global paramaters
'server_name': server_name,
'server_uptime': server_uptime, 'server_uptime': server_uptime,
'action': state, 'action': state.title(),
'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')), 'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')), 'timestamp': arrow.now().format(time_format),
# Stream parameters
'streams': stream_count, 'streams': stream_count,
'user': user, 'user': session.get('friendly_name',''),
'platform': platform, 'platform': session.get('platform',''),
'player': player, 'player': session.get('player',''),
'ip_address': ip_address, 'ip_address': session.get('ip_address','N/A'),
'media_type': metadata['media_type'],
'stream_duration': stream_duration, 'stream_duration': stream_duration,
'remaining_duration': duration - view_offset, 'stream_time': arrow.get(stream_duration * 60).format(duration_format),
'progress': view_offset, '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, 'progress_percent': progress_percent,
'container': container, 'container': session.get('container',''),
'video_codec': video_codec, 'video_codec': session.get('video_codec',''),
'video_bitrate': video_bitrate, 'video_bitrate': session.get('bitrate',''),
'video_width': video_width, 'video_width': session.get('width',''),
'video_height': video_height, 'video_height': session.get('height',''),
'video_resolution': video_resolution, 'video_resolution': session.get('video_resolution',''),
'video_framerate': video_framerate, 'video_framerate': session.get('video_framerate',''),
'aspect_ratio': aspect_ratio, 'aspect_ratio': session.get('aspect_ratio',''),
'audio_codec': audio_codec, 'audio_codec': session.get('audio_codec',''),
'audio_channels': audio_channels, 'audio_channels': session.get('audio_channels',''),
'transcode_decision': transcode_decision, 'transcode_decision': transcode_decision,
'video_decision': video_decision, 'video_decision': session.get('video_decision','').title(),
'audio_decision': audio_decision, 'audio_decision': session.get('audio_decision','').title(),
'transcode_container': transcode_container, 'transcode_container': session.get('transcode_container',''),
'transcode_video_codec': transcode_video_codec, 'transcode_video_codec': session.get('transcode_video_codec',''),
'transcode_video_width': transcode_video_width, 'transcode_video_width': session.get('transcode_width',''),
'transcode_video_height': transcode_video_height, 'transcode_video_height': session.get('transcode_height',''),
'transcode_audio_codec': transcode_audio_codec, 'transcode_audio_codec': session.get('transcode_audio_codec',''),
'transcode_audio_channels': transcode_audio_channels, 'transcode_audio_channels': session.get('transcode_audio_channels',''),
'user_id': user_id, 'session_key': session.get('session_key',''),
'user_id': session.get('user_id',''),
# Metadata parameters
'media_type': metadata['media_type'],
'title': full_title, 'title': full_title,
'library_name': metadata['library_name'], 'library_name': metadata['library_name'],
'show_name': show_name, 'show_name': show_name,
@ -575,7 +537,7 @@ def build_notify_text(session=None, timeline=None, state=None):
'summary': metadata['summary'], 'summary': metadata['summary'],
'tagline': metadata['tagline'], 'tagline': metadata['tagline'],
'rating': metadata['rating'], 'rating': metadata['rating'],
'duration': duration, 'duration': metadata['duration'],
'section_id': metadata['section_id'], 'section_id': metadata['section_id'],
'rating_key': metadata['rating_key'], 'rating_key': metadata['rating_key'],
'parent_rating_key': metadata['parent_rating_key'], 'parent_rating_key': metadata['parent_rating_key'],
@ -598,9 +560,9 @@ def build_notify_text(session=None, timeline=None, state=None):
if state == 'play': if state == 'play':
# Default body text # Default body text
body_text = '%s (%s) is watching %s' % (session['friendly_name'], body_text = '%s (%s) started playing %s' % (session['friendly_name'],
session['player'], session['player'],
full_title) full_title)
if on_start_subject and on_start_body: if on_start_subject and on_start_body:
try: try:
@ -767,6 +729,10 @@ def build_notify_text(session=None, timeline=None, state=None):
def build_server_notify_text(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 # Get the server name
server_name = plexpy.CONFIG.PMS_NAME server_name = plexpy.CONFIG.PMS_NAME
@ -791,11 +757,12 @@ def build_server_notify_text(state=None):
on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT
script_args_text = plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_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, 'server_uptime': server_uptime,
'action': state, 'action': state.title(),
'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')), 'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz',''))} 'timestamp': arrow.now().format(time_format)}
# Default text # Default text
subject_text = 'PlexPy (%s)' % server_name subject_text = 'PlexPy (%s)' % server_name
@ -907,4 +874,4 @@ def build_server_notify_text(state=None):
def strip_tag(data): def strip_tag(data):
p = re.compile(r'<.*?>') 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 gntp.notifier
import oauth2 as oauth import oauth2 as oauth
import pythontwitter as twitter import pythontwitter as twitter
import pythonfacebook as facebook import pythonfacebook as facebook
import plexpy import plexpy
from plexpy import logger, helpers, request from plexpy import logger, helpers, request
@ -58,7 +58,7 @@ AGENT_IDS = {"Growl": 0,
"Scripts": 15, "Scripts": 15,
"Facebook": 16} "Facebook": 16}
def available_notification_agents(): def available_notification_agents():
agents = [{'name': 'Growl', agents = [{'name': 'Growl',
'id': AGENT_IDS['Growl'], 'id': AGENT_IDS['Growl'],
@ -1777,7 +1777,7 @@ class SLACK(object):
class Scripts(object): class Scripts(object):
def __init__(self, **kwargs): 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): def conf(self, options):
return cherrypy.config['config'].get('Scripts', options) return cherrypy.config['config'].get('Scripts', options)
@ -1807,7 +1807,7 @@ class Scripts(object):
return scripts 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: Args:
subject(string, optional): Head text, 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" % 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)) (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: if not plexpy.CONFIG.SCRIPTS_FOLDER:
return return
@ -1869,14 +1872,16 @@ class Scripts(object):
name, ext = os.path.splitext(script) name, ext = os.path.splitext(script)
if ext == '.py': if ext == '.php':
prefix = 'python'
elif ext == '.pyw':
prefix = 'pythonw'
elif ext == '.php':
prefix = 'php' prefix = 'php'
elif ext == '.pl': elif ext == '.pl':
prefix = 'perl' prefix = 'perl'
elif ext == '.ps1':
prefix = 'powershell -executionPolicy bypass -file'
elif ext == '.py':
prefix = 'python'
elif ext == '.pyw':
prefix = 'pythonw'
elif ext == '.rb': elif ext == '.rb':
prefix = 'ruby' prefix = 'ruby'
else: else:
@ -1886,7 +1891,7 @@ class Scripts(object):
script = script.encode(plexpy.SYS_ENCODING, 'ignore') script = script.encode(plexpy.SYS_ENCODING, 'ignore')
if prefix: if prefix:
script = [prefix, script] script = prefix.split() + [script]
else: else:
script = [script] script = [script]
@ -2025,7 +2030,7 @@ class Scripts(object):
return config_option return config_option
class FacebookNotifier(object): class FacebookNotifier(object):
def __init__(self): def __init__(self):
@ -2050,7 +2055,7 @@ class FacebookNotifier(object):
def _get_credentials(self, code): def _get_credentials(self, code):
logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook") logger.info(u"PlexPy Notifiers :: Requesting access token from Facebook")
try: try:
# Request user access token # Request user access token
api = facebook.GraphAPI(version='2.5') api = facebook.GraphAPI(version='2.5')
@ -2059,19 +2064,19 @@ class FacebookNotifier(object):
app_id=self.app_id, app_id=self.app_id,
app_secret=self.app_secret) app_secret=self.app_secret)
access_token = response['access_token'] access_token = response['access_token']
# Request extended user access token # Request extended user access token
api = facebook.GraphAPI(access_token=access_token, version='2.5') api = facebook.GraphAPI(access_token=access_token, version='2.5')
response = api.extend_access_token(app_id=self.app_id, response = api.extend_access_token(app_id=self.app_id,
app_secret=self.app_secret) app_secret=self.app_secret)
access_token = response['access_token'] access_token = response['access_token']
plexpy.CONFIG.FACEBOOK_TOKEN = access_token plexpy.CONFIG.FACEBOOK_TOKEN = access_token
plexpy.CONFIG.write() plexpy.CONFIG.write()
except Exception as e: except Exception as e:
logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e) logger.error(u"PlexPy Notifiers :: Error requesting Facebook access token: %s" % e)
return False return False
return True return True
def _post_facebook(self, message=None): def _post_facebook(self, message=None):

View file

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

View file

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

View file

@ -492,7 +492,20 @@ 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'})
else: 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 ##### ##### Users #####
@ -1382,7 +1395,11 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs): 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() plex_tv = plextv.PlexTV()
servers = plex_tv.discover() servers = plex_tv.discover()
@ -1391,27 +1408,28 @@ class WebInterface(object):
identifier = server['clientIdentifier'] identifier = server['clientIdentifier']
break break
if identifier and hostname and port: # Fallback to checking /identity endpoint is server is unpublished
# Set PMS attributes to get the real PMS url # Cannot set SSL settings on the PMS if unpublished so 'http' is okay
plexpy.CONFIG.__setattr__('PMS_IP', hostname) if not identifier:
plexpy.CONFIG.__setattr__('PMS_PORT', port) request_handler = http_handler.HTTPHandler(host=hostname,
plexpy.CONFIG.__setattr__('PMS_IDENTIFIER', identifier) port=port,
plexpy.CONFIG.__setattr__('PMS_SSL', ssl) token=None)
plexpy.CONFIG.__setattr__('PMS_IS_REMOTE', remote) uri = '/identity'
plexpy.CONFIG.write() request = request_handler.make_request(uri=uri,
proto='http',
plextv.get_real_pms_url() request_type='GET',
output_format='xml',
pms_connect = pmsconnect.PmsConnect() no_token=True,
request = pms_connect.get_local_server_identity() timeout=10)
if request:
if request: xml_head = request.getElementsByTagName('MediaContainer')[0]
cherrypy.response.headers['Content-type'] = 'application/xml' identifier = xml_head.getAttribute('machineIdentifier')
return request
else: if identifier:
logger.warn(u"Unable to retrieve data for get_server_id.") cherrypy.response.headers['Content-type'] = 'application/json'
return None return json.dumps(identifier)
else: else:
logger.warn('Unable to retrieve the PMS identifier.')
return None return None
@cherrypy.expose @cherrypy.expose