diff --git a/data/interfaces/default/welcome.html b/data/interfaces/default/welcome.html index 89c21492..bc983771 100644 --- a/data/interfaces/default/welcome.html +++ b/data/interfaces/default/welcome.html @@ -1,6 +1,6 @@ <% - import jellypy - from jellypy import common, helpers +import jellypy +from jellypy import common, helpers %> @@ -69,7 +69,8 @@
- +
@@ -77,18 +78,20 @@
- +
- +
-

Jellyfin

+

Jellyfin Server

Select your Jellyfin Server from the dropdown menu or enter an IP address or hostname. @@ -108,7 +111,8 @@ data-ssl="${config['pms_ssl']}" data-is_cloud="${config['pms_is_cloud']}" data-label="${config['pms_name'] or 'Local'}" - selected>${config['pms_ip']} + selected>${config['pms_ip']} + % endif

@@ -118,12 +122,15 @@
- +
@@ -131,22 +138,50 @@
- - +
+ +
+

Jellyfin Auth

+
+

+ Authentificate with your Jellyfin instance. +

+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
Verify
-
+

Activity Logging

@@ -157,38 +192,47 @@

- +
- +
-

The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.

+

The interval (in seconds) an item must be in a playing state before + logging it. 0 to disable.

- Additional options to disable history logging for certain libraries or users can be found by editing them + Additional options to disable history logging for certain libraries or users can be found by + editing them on the Libraries or Users pages.

-
+

Notifications

- Tautulli can send a wide variety of notifications to alert you of activity on your Plex server. + Tautulli can send a wide variety of notifications to alert you of activity on your Plex + server.

To set up a notification agent, navigate to the Settings page - and to the Notification Agents tab after you have completed this setup wizard. + and to the Notification Agents tab after you have completed this setup + wizard.

-
+

Database Import

- If you have an existing Tautulli, PlexWatch, or Plexivity database, you can import the data into Tautulli. + If you have an existing Tautulli, PlexWatch, or Plexivity database, you can import the data + into Tautulli.

To import a database, navigate to the Settings page @@ -200,18 +244,23 @@

- + - - + + - - + + @@ -224,9 +273,11 @@

Setup Complete!


-

Setup is now complete. For more configuration options please visit the Settings menu on the home page.

+

Setup is now complete. For more configuration options please visit the Settings menu on the home + page.


-  Waiting 5 seconds to ensure authentication token is registered... +  Waiting 5 seconds to ensure + authentication token is registered...
@@ -243,7 +294,6 @@ function validateAuthentication(el) { var http_username = $("#http_username").val(); var http_password = $("#http_password").val(); - var valid_authentication = el.val(); var retValue = {}; if (http_username === "" || http_password === "") { @@ -310,71 +360,73 @@ return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0) } -$(document).ready(function() { + $(document).ready(function () { - $.fn.wizard.logging = false; - var options = { - keyboard : false, - contentHeight : 450, - contentWidth : 700, - backdrop: 'static', - buttons: {submitText: 'Finish'}, - submitUrl: "configUpdate" - }; - var wizard = $("#setup-wizard").wizard(options); - wizard.show(); + $.fn.wizard.logging = false; + var options = { + keyboard: false, + contentHeight: 450, + contentWidth: 700, + backdrop: 'static', + buttons: {submitText: 'Finish'}, + submitUrl: "configUpdate" + }; + var wizard = $("#setup-wizard").wizard(options); + wizard.show(); - // Change button classes - wizard.find('.wizard-back').addClass('btn-dark'); - wizard.on('incrementCard', function(wizard) { - wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright'); - }); - wizard.on('decrementCard', function(wizard) { - wizard.find('.wizard-next').removeClass('btn-bright').text('Next'); - }); + // Change button classes + wizard.find('.wizard-back').addClass('btn-dark'); + wizard.on('incrementCard', function (wizard) { + wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright'); + }); + wizard.on('decrementCard', function (wizard) { + wizard.find('.wizard-next').removeClass('btn-bright').text('Next'); + }); - wizard.on("submit", function(wizard) { - // Probably should not success before we know, but hopefully validation is good enough. - wizard.submitSuccess(); - $.ajax({ - type: "POST", - url: wizard.args.submitUrl, - data: wizard.serialize(), - dataType: "json", - complete: function (data) { - $(".countdown").countdown(function () { location.reload(); }, 5, ""); - } - }) - }); - - $('.checkbox-toggle').click(function () { - var configToggle = $(this).data('id'); - if ($(this).is(':checked')) { - $('#'+configToggle).val(1); - } else { - $('#'+configToggle).val(0); - } - }); - - var $select_pms = $('#pms_ip_selectize').selectize({ - createOnBlur: true, - openOnFocus: true, - maxItems: 1, - closeAfterSelect: true, - sortField: 'label', - searchField: ['label', 'value'], - inputClass: 'form-control selectize-input', - render: { - item: function (item, escape) { - if (!item.label) { - $.extend(item, - $(this.revertSettings.$children) - .filter('[value="' + item.value + '"]').data() - ); + wizard.on("submit", function (wizard) { + // Probably should not success before we know, but hopefully validation is good enough. + wizard.submitSuccess(); + $.ajax({ + type: "POST", + url: wizard.args.submitUrl, + data: wizard.serialize(), + dataType: "json", + complete: function (data) { + $(".countdown").countdown(function () { + location.reload(); + }, 5, ""); } - var label = item.label || item.value; - var caption = item.label ? item.ip : null; - return '
' + escape(label) + '' + (caption ? '' + escape(caption) + '' : '') + '
'; - }, - option: function (item, escape) { - var label = item.label || item.value; - var caption = item.label ? item.value : null; - return '
' + escape(caption) + '' : '') + '
'; - } - }, - create: function(input) { - return {label: '', value: input}; - }, - onInitialize: function () { - var s = this; - this.revertSettings.$children.each(function () { - $.extend(s.options[this.value], $(this).data()); - }); - }, - onChange: function (item) { - var pms_ip_selected = this.getItem(item)[0]; - var identifier = $(pms_ip_selected).data('identifier'); - var ip = $(pms_ip_selected).data('ip'); - var port = $(pms_ip_selected).data('port'); - var local = $(pms_ip_selected).data('local'); - var ssl = $(pms_ip_selected).data('ssl'); - var is_cloud = $(pms_ip_selected).data('is_cloud'); - var value = $(pms_ip_selected).data('value'); + } + }, + create: function (input) { + return {label: '', value: input}; + }, + onInitialize: function () { + var s = this; + this.revertSettings.$children.each(function () { + $.extend(s.options[this.value], $(this).data()); + }); + }, + onChange: function (item) { + var pms_ip_selected = this.getItem(item)[0]; + var identifier = $(pms_ip_selected).data('identifier'); + var ip = $(pms_ip_selected).data('ip'); + var port = $(pms_ip_selected).data('port'); + var local = $(pms_ip_selected).data('local'); + var ssl = $(pms_ip_selected).data('ssl'); + var value = $(pms_ip_selected).data('value'); - $("#pms_valid").val(identifier !== 'undefined' ? 'valid' : ''); - $("#pms-verify-status").html(identifier !== 'undefined' ? '  Server found!' : '').fadeIn('fast'); + $("#pms_valid").val(identifier !== 'undefined' ? 'valid' : ''); + $("#pms-verify-status").html(identifier !== 'undefined' ? '  Server found!' : '').fadeIn('fast'); + + $("#pms_identifier").val(identifier !== 'undefined' ? identifier : ''); + $('#pms_ip').val(ip !== 'undefined' ? ip : value); + $('#pms_port').val(port !== 'undefined' ? port : 8096); + $('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0)); + $('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0); + $('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1)); + $('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0); - $("#pms_identifier").val(identifier !== 'undefined' ? identifier : ''); - $('#pms_ip').val(ip !== 'undefined' ? ip : value); - $('#pms_port').val(port !== 'undefined' ? port : 32400); - $('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0)); - $('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0); - $('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1)); - $('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0); - $('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0); - if (is_cloud === true) { - $('#pms_port').prop('readonly', true); - $('#pms_is_remote_checkbox').prop('disabled', true); - $('#pms_ssl_checkbox').prop('disabled', true); - } else { $('#pms_port').prop('readonly', false); $('#pms_is_remote_checkbox').prop('disabled', false); $('#pms_ssl_checkbox').prop('disabled', false); - } - }, - onDropdownOpen: function() { - this.clear(); - } - }); - var select_pms = $select_pms[0].selectize; - - function getServerOptions(token) { - /* Set token and returns server options */ - $.ajax({ - url: 'discover', - data: { - token: token }, - success: function (result) { - if (result) { - var existing_ip = $('#pms_ip').val(); - var existing_port = $('#pms_port').val(); - result.forEach(function (item) { - if (item.ip === existing_ip && item.port === existing_port) { - select_pms.updateOption(item.value, item); - } else { - select_pms.addOption(item); + onDropdownOpen: function () { + this.clear(); + } + }); + var select_pms = $select_pms[0].selectize; + + function getServerOptions(token) { + /* Set token and returns server options */ + $.ajax({ + url: 'discover', + data: { + token: token + }, + success: function (result) { + if (result) { + var existing_ip = $('#pms_ip').val(); + var existing_port = $('#pms_port').val(); + result.forEach(function (item) { + if (item.ip === existing_ip && item.port === existing_port) { + select_pms.updateOption(item.value, item); + } else { + select_pms.addOption(item); + } + }); + } + } + }) + } + + var pms_verified = false; + var authenticated = false; + + $("#verify-plex-server").click(function () { + if (!(pms_verified)) { + var pms_ip = $("#pms_ip").val().trim(); + var pms_port = $("#pms_port").val().trim(); + var pms_identifier = $("#pms_identifier").val(); + var pms_ssl = $("#pms_ssl").val(); + var pms_is_remote = $("#pms_is_remote").val(); + if ((pms_ip !== '') || (pms_port !== '')) { + $("#pms-verify-status").html('  Verifying server...'); + $('#pms-verify-status').fadeIn('fast'); + $.ajax({ + url: 'get_server_id', + data: { + hostname: pms_ip, + port: pms_port, + identifier: pms_identifier, + ssl: pms_ssl, + remote: pms_is_remote + }, + cache: true, + async: true, + timeout: 5000, + error: function (jqXHR, textStatus, errorThrown) { + $("#pms-verify-status").html('  Error verifying server: ' + textStatus); + $('#pms-verify-status').fadeIn('fast'); + }, + success: function (xhr, status) { + var result = xhr; + var identifier = result.identifier; + if (identifier) { + $("#pms_identifier").val(identifier); + $("#pms-verify-status").html('  Server found!'); + $('#pms-verify-status').fadeIn('fast'); + pms_verified = true; + $("#pms_valid").val("valid"); + } else { + $("#pms-verify-status").html('  This is not a Plex Server!'); + $('#pms-verify-status').fadeIn('fast'); + } } }); + } else { + $("#pms-verify-status").html('  Please enter both fields.'); + $('#pms-verify-status').fadeIn('fast'); } } - }) - } - var pms_verified = false; - var authenticated = false; + }); - $("#verify-plex-server").click(function () { - if (!(pms_verified)) { - var pms_ip = $("#pms_ip").val().trim(); - var pms_port = $("#pms_port").val().trim(); - var pms_identifier = $("#pms_identifier").val(); - var pms_ssl = $("#pms_ssl").val(); - var pms_is_remote = $("#pms_is_remote").val(); - if ((pms_ip !== '') || (pms_port !== '')) { - $("#pms-verify-status").html('  Verifying server...'); - $('#pms-verify-status').fadeIn('fast'); - $.ajax({ - url: 'get_server_id', - data: { - hostname: pms_ip, - port: pms_port, - identifier: pms_identifier, - ssl: pms_ssl, - remote: pms_is_remote - }, - cache: true, - async: true, - timeout: 5000, - error: function (jqXHR, textStatus, errorThrown) { - $("#pms-verify-status").html('  Error verifying server: ' + textStatus); - $('#pms-verify-status').fadeIn('fast'); - }, - success: function(xhr, status) { - var result = xhr; - var identifier = result.identifier; - if (identifier) { - $("#pms_identifier").val(identifier); - $("#pms-verify-status").html('  Server found!'); - $('#pms-verify-status').fadeIn('fast'); - pms_verified = true; - $("#pms_valid").val("valid"); - } else { - $("#pms-verify-status").html('  This is not a Plex Server!'); - $('#pms-verify-status').fadeIn('fast'); - } - } - }); - } else { - $("#pms-verify-status").html('  Please enter both fields.'); - $('#pms-verify-status').fadeIn('fast'); - } - } + $(".pms-settings").change(function () { + pms_verified = false; + $("#pms_valid").val(""); + $("#pms-verify-status").html(""); + }); }); - - $( ".pms-settings" ).change(function() { - pms_verified = false; - $("#pms_valid").val(""); - $("#pms-verify-status").html(""); - }); - - function OAuthPreFunction() { - $("#pms_token").val(''); - $("#pms-token-status").html('  Waiting for authentication...').fadeIn('fast'); - } - function OAuthSuccessCallback(authToken) { - $("#pms_token").val(authToken); - $("#pms-token-status").html('  Authentication successful.').fadeIn('fast'); - authenticated = true; - getServerOptions(authToken); - } - function OAuthErrorCallback() { - $("#pms-token-status").html('  Error communicating with Plex.tv.').fadeIn('fast'); - } - - $('#sign-in-plex').click(function() { - PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback, OAuthPreFunction); - }); -}); diff --git a/jellypy/datafactory.py b/jellypy/datafactory.py index 1e225f6e..03663f22 100644 --- a/jellypy/datafactory.py +++ b/jellypy/datafactory.py @@ -15,11 +15,9 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -import json from itertools import groupby import jellypy - from jellypy import common from jellypy import database from jellypy import datatables @@ -1607,38 +1605,38 @@ class DataFactory(object): if old_key_list and new_key_list: mapping = get_pairs(old_key_list, new_key_list) - # TODO: Jellyfin - # if mapping: - # logger.info("Tautulli DataFactory :: Updating metadata in the database.") - # for old_key, new_key in mapping.items(): - # metadata = pms_connect.get_metadata_details(new_key) - # - # if metadata: - # if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist': - # # check grandparent_rating_key (2 tables) - # monitor_db.action( - # 'UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', - # [new_key, old_key]) - # monitor_db.action( - # 'UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', - # [new_key, old_key]) - # elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album': - # # check parent_rating_key (2 tables) - # monitor_db.action( - # 'UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?', - # [new_key, old_key]) - # monitor_db.action( - # 'UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?', - # [new_key, old_key]) - # else: - # # check rating_key (2 tables) - # monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?', - # [new_key, old_key]) - # monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?', - # [new_key, old_key]) - # - # # update session_history_metadata table - # self.update_metadata_details(old_key, new_key, metadata) + # TODO: Jellyfin + # if mapping: + # logger.info("Tautulli DataFactory :: Updating metadata in the database.") + # for old_key, new_key in mapping.items(): + # metadata = pms_connect.get_metadata_details(new_key) + # + # if metadata: + # if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist': + # # check grandparent_rating_key (2 tables) + # monitor_db.action( + # 'UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', + # [new_key, old_key]) + # monitor_db.action( + # 'UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', + # [new_key, old_key]) + # elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album': + # # check parent_rating_key (2 tables) + # monitor_db.action( + # 'UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?', + # [new_key, old_key]) + # monitor_db.action( + # 'UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?', + # [new_key, old_key]) + # else: + # # check rating_key (2 tables) + # monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?', + # [new_key, old_key]) + # monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?', + # [new_key, old_key]) + # + # # update session_history_metadata table + # self.update_metadata_details(old_key, new_key, metadata) return 'Updated metadata in database.' else: diff --git a/jellypy/helpers.py b/jellypy/helpers.py index a5eeca43..1522cc5c 100644 --- a/jellypy/helpers.py +++ b/jellypy/helpers.py @@ -37,15 +37,11 @@ from urllib.parse import urlencode from xml.dom import minidom import arrow -import cloudinary import ipwhois import ipwhois.exceptions import ipwhois.utils import xmltodict from IPy import IP -from cloudinary.api import delete_resources_by_tag -from cloudinary.uploader import upload -from cloudinary.utils import cloudinary_url import jellypy from jellypy import common @@ -795,110 +791,6 @@ def delete_from_imgur(delete_hash, img_title='', fallback=''): return False -def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''): - """ Uploads an image to Cloudinary """ - img_url = '' - - if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET: - logger.error( - "Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.") - return img_url - - cloudinary.config( - cloud_name=jellypy.CONFIG.CLOUDINARY_CLOUD_NAME, - api_key=jellypy.CONFIG.CLOUDINARY_API_KEY, - api_secret=jellypy.CONFIG.CLOUDINARY_API_SECRET - ) - - # Cloudinary library has very poor support for non-ASCII characters on Python 2 - if jellypy.PYTHON2: - _img_title = latinToAscii(img_title, replace=True) - else: - _img_title = img_title - - try: - response = upload((img_title, img_data), - public_id='{}_{}'.format(fallback, rating_key), - tags=['tautulli', fallback, str(rating_key)], - context={'title': _img_title, 'rating_key': str(rating_key), 'fallback': fallback}) - logger.debug("Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback)) - img_url = response.get('url', '') - except Exception as e: - logger.error( - "Tautulli Helpers :: Unable to upload image '{}' ({}) to Cloudinary: {}".format(img_title, fallback, e)) - - return img_url - - -def delete_from_cloudinary(rating_key=None, delete_all=False): - """ Deletes an image from Cloudinary """ - if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET: - logger.error( - "Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.") - return False - - cloudinary.config( - cloud_name=jellypy.CONFIG.CLOUDINARY_CLOUD_NAME, - api_key=jellypy.CONFIG.CLOUDINARY_API_KEY, - api_secret=jellypy.CONFIG.CLOUDINARY_API_SECRET - ) - - if delete_all: - delete_resources_by_tag('tautulli') - logger.debug("Tautulli Helpers :: Deleted all images from Cloudinary.") - elif rating_key: - delete_resources_by_tag(str(rating_key)) - logger.debug("Tautulli Helpers :: Deleted images from Cloudinary with rating_key {}.".format(rating_key)) - else: - logger.debug("Tautulli Helpers :: Unable to delete images from Cloudinary: No rating_key provided.") - - return True - - -def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100, background='000000', blur=0, - img_format='png', img_title='', fallback=None): - url = '' - - if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET: - logger.error( - "Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.") - return url - - cloudinary.config( - cloud_name=jellypy.CONFIG.CLOUDINARY_CLOUD_NAME, - api_key=jellypy.CONFIG.CLOUDINARY_API_KEY, - api_secret=jellypy.CONFIG.CLOUDINARY_API_SECRET - ) - - img_options = {'format': img_format, - 'fetch_format': 'auto', - 'quality': 'auto', - 'version': timestamp(), - 'secure': True} - - if width != 1000: - img_options['width'] = str(width) - img_options['crop'] = 'fill' - if height != 1500: - img_options['height'] = str(height) - img_options['crop'] = 'fill' - if opacity != 100: - img_options['opacity'] = opacity - if background != '000000': - img_options['background'] = 'rgb:{}'.format(background) - if blur != 0: - img_options['effect'] = 'blur:{}'.format(blur * 100) - - try: - url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options) - logger.debug("Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback)) - except Exception as e: - logger.error( - "Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e)) - - return url - - def cache_image(url, image=None): """ Saves an image to the cache directory. diff --git a/jellypy/jellyfin.py b/jellypy/jellyfin.py index 0f024523..5ade0b4d 100644 --- a/jellypy/jellyfin.py +++ b/jellypy/jellyfin.py @@ -19,7 +19,7 @@ from jellyfin_apiclient_python import JellyfinClient class Jellyfin(object): - def __init__(self, url, token): + def __init__(self, url, token=None): self.jf = JellyfinClient() def get_library(self, section_id): @@ -30,3 +30,6 @@ class Jellyfin(object): def get_item(self, rating_key): return self.jf.fetchItem(rating_key) + + def login(self, user, password): + pass