@@ -684,6 +684,15 @@
pushoverPriority();
});
+ % elif notifier['agent_name'] == 'plexmobileapp':
+ var $plexmobileapp_user_ids = $('#plexmobileapp_user_ids').selectize({
+ plugins: ['remove_button'],
+ maxItems: null,
+ create: true
+ });
+ var plexmobileapp_user_ids = $plexmobileapp_user_ids[0].selectize;
+ plexmobileapp_user_ids.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'plexmobileapp_user_ids'), [])) | n});
+
% endif
function validateLogic() {
diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html
index 31628c26..20fe51a6 100644
--- a/data/interfaces/default/settings.html
+++ b/data/interfaces/default/settings.html
@@ -941,7 +941,7 @@
-
The value (in seconds) Tautulli should wait before triggering the next buffer warning. 0 to always trigger.
+
The value (in seconds) Tautulli should wait before triggering the next buffer warning. Set to 0 to always trigger.
The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.
+
+
+
+
+
+
+
+
+
+ The number of seconds between stopping and starting a new stream to be considered as a continued session. Set to 0 to consider all streams as new sessions.
+
+ Note: The threshold is only used by the "Initial Stream" notification parameter to determine if a stream is the first stream of a continuous streaming session.
+
+
Recently Added Notifications
@@ -1254,7 +1268,7 @@
Enable to lookup links to MusicBrainz for music when available.
-
+
Delete all cached metadata lookup info in Tautulli.
@@ -1267,54 +1281,6 @@
-
-
Geolocation Database
-
-
-
The GeoLite2 database is used to geolocate IP addresses.
- Enter your MaxMind License Key to install the GeoLite2 database.
-
-
-
- ${docker_msg | n}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Leave blank to install in the default location. GeoLite2 database last updated never.
-
-
-
-
-
-
-
-
-
-
-
The interval (in days) Tautulli will automatically update the GeoLite2 database. Minimum 7, maximum 30, default 30.
-
-
@@ -2928,56 +2894,6 @@ $(document).ready(function() {
$('#resources-xml').on('tripleclick', function () {
openPlexXML('/api/resources', true, {includeHttps: 1});
});
-
- if ("${kwargs.get('install_geoip')}" === 'true') {
- gotoSetting('3rd_party_apis', 'geoip_db')
- }
-
- if ("${config['geoip_db_installed']}" > "0") {
- $("#geoip_db_updated").text(moment("${config['geoip_db_installed']}", "X").fromNow());
- }
-
- $("#install_geoip_db").click(function () {
- var maxmind_license_key = $("#maxmind_license_key");
- maxmind_license_key.val($.trim(maxmind_license_key.val()));
- if (maxmind_license_key.val() === "") {
- maxmind_license_key.focus();
- showMsg(' Maxmind License Key is required.', false, true, 5000, true);
- return false;
- } else if (!(saveSettings())) {
- return false;
- }
- var msg = 'Are you sure you want to install the GeoLite2 database?
' +
- 'The database is used to lookup IP address geolocation info. ' +
- 'The database will be downloaded from MaxMind, ' +
- 'and requires 100MB of free space to install. ';
- var url = 'install_geoip_db';
- if ($(this).text() === 'Update') {
- url += '?update=true';
- }
- confirmAjaxCall(url, msg, null, 'Installing GeoLite2 database.', function (result) {
- if (result.result === "success") {
- $('#install_geoip_db').text('Update');
- $('#uninstall_geoip_db').prop('disabled', false);
- $('#geoip_db_updated').text(moment(result.updated, "X").fromNow());
- }
- getSchedulerTable();
- });
- });
-
- $("#uninstall_geoip_db").click(function () {
- var msg = 'Are you sure you want to uninstall the GeoLite2 database?
' +
- 'You will not be able to lookup IP address geolocation info.';
- var url = 'uninstall_geoip_db';
- confirmAjaxCall(url, msg, null, 'Uninstalling GeoLite2 database.', function (result) {
- if (result.result === "success") {
- $('#install_geoip_db').text('Install');
- $('#uninstall_geoip_db').prop('disabled', true);
- $('#geoip_db_updated').text('never');
- }
- getSchedulerTable();
- });
- });
});
%def>
diff --git a/data/interfaces/default/user.html b/data/interfaces/default/user.html
index 0051afec..b2af7a37 100644
--- a/data/interfaces/default/user.html
+++ b/data/interfaces/default/user.html
@@ -582,9 +582,9 @@ DOCUMENTATION :: END
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
+ history_table.draw();
}
});
- history_table.draw();
});
}
diff --git a/lib/geoip2/__init__.py b/lib/geoip2/__init__.py
deleted file mode 100644
index 590124a9..00000000
--- a/lib/geoip2/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# pylint:disable=C0111
-
-__title__ = 'geoip2'
-__version__ = '2.4.0'
-__author__ = 'Gregory Oschwald'
-__license__ = 'Apache License, Version 2.0'
-__copyright__ = 'Copyright (c) 2013-2016 Maxmind, Inc.'
diff --git a/lib/geoip2/compat.py b/lib/geoip2/compat.py
deleted file mode 100644
index 67c5fa65..00000000
--- a/lib/geoip2/compat.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Intended for internal use only."""
-import sys
-
-import ipaddress
-
-# pylint: skip-file
-
-if sys.version_info[0] == 2:
- def compat_ip_address(address):
- """Intended for internal use only."""
- if isinstance(address, bytes):
- address = address.decode()
- return ipaddress.ip_address(address)
-else:
- def compat_ip_address(address):
- """Intended for internal use only."""
- return ipaddress.ip_address(address)
diff --git a/lib/geoip2/database.py b/lib/geoip2/database.py
deleted file mode 100644
index ed21d6d4..00000000
--- a/lib/geoip2/database.py
+++ /dev/null
@@ -1,199 +0,0 @@
-"""
-======================
-GeoIP2 Database Reader
-======================
-
-"""
-import inspect
-
-import maxminddb
-# pylint: disable=unused-import
-from maxminddb import (MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, MODE_FILE,
- MODE_MEMORY)
-
-import geoip2
-import geoip2.models
-import geoip2.errors
-
-
-class Reader(object):
- """GeoIP2 database Reader object.
-
- Instances of this class provide a reader for the GeoIP2 database format.
- IP addresses can be looked up using the ``country`` and ``city`` methods.
-
- The basic API for this class is the same for every database. First, you
- create a reader object, specifying a file name. You then call the method
- corresponding to the specific database, passing it the IP address you want
- to look up.
-
- If the request succeeds, the method call will return a model class for the
- method you called. This model in turn contains multiple record classes,
- each of which represents part of the data returned by the database. If the
- database does not contain the requested information, the attributes on the
- record class will have a ``None`` value.
-
- If the address is not in the database, an
- ``geoip2.errors.AddressNotFoundError`` exception will be thrown. If the
- database is corrupt or invalid, a ``maxminddb.InvalidDatabaseError`` will
- be thrown.
-
-"""
-
- def __init__(self, filename, locales=None, mode=MODE_AUTO):
- """Create GeoIP2 Reader.
-
- :param filename: The path to the GeoIP2 database.
- :param locales: This is list of locale codes. This argument will be
- passed on to record classes to use when their name properties are
- called. The default value is ['en'].
-
- The order of the locales is significant. When a record class has
- multiple names (country, city, etc.), its name property will return
- the name in the first locale that has one.
-
- Note that the only locale which is always present in the GeoIP2
- data is "en". If you do not include this locale, the name property
- may end up returning None even when the record has an English name.
-
- Currently, the valid locale codes are:
-
- * de -- German
- * en -- English names may still include accented characters if that
- is the accepted spelling in English. In other words, English does
- not mean ASCII.
- * es -- Spanish
- * fr -- French
- * ja -- Japanese
- * pt-BR -- Brazilian Portuguese
- * ru -- Russian
- * zh-CN -- Simplified Chinese.
- :param mode: The mode to open the database with. Valid mode are:
- * MODE_MMAP_EXT - use the C extension with memory map.
- * MODE_MMAP - read from memory map. Pure Python.
- * MODE_FILE - read database as standard file. Pure Python.
- * MODE_MEMORY - load database into memory. Pure Python.
- * MODE_AUTO - try MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that order.
- Default.
-
- """
- if locales is None:
- locales = ['en']
- self._db_reader = maxminddb.open_database(filename, mode)
- self._locales = locales
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- self.close()
-
- def country(self, ip_address):
- """Get the Country object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.Country` object
-
- """
-
- return self._model_for(geoip2.models.Country, 'Country', ip_address)
-
- def city(self, ip_address):
- """Get the City object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.City` object
-
- """
- return self._model_for(geoip2.models.City, 'City', ip_address)
-
- def anonymous_ip(self, ip_address):
- """Get the AnonymousIP object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.AnonymousIP` object
-
- """
- return self._flat_model_for(geoip2.models.AnonymousIP,
- 'GeoIP2-Anonymous-IP', ip_address)
-
- def connection_type(self, ip_address):
- """Get the ConnectionType object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.ConnectionType` object
-
- """
- return self._flat_model_for(geoip2.models.ConnectionType,
- 'GeoIP2-Connection-Type', ip_address)
-
- def domain(self, ip_address):
- """Get the Domain object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.Domain` object
-
- """
- return self._flat_model_for(geoip2.models.Domain, 'GeoIP2-Domain',
- ip_address)
-
- def enterprise(self, ip_address):
- """Get the Enterprise object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.Enterprise` object
-
- """
- return self._model_for(geoip2.models.Enterprise, 'Enterprise',
- ip_address)
-
- def isp(self, ip_address):
- """Get the ISP object for the IP address.
-
- :param ip_address: IPv4 or IPv6 address as a string.
-
- :returns: :py:class:`geoip2.models.ISP` object
-
- """
- return self._flat_model_for(geoip2.models.ISP, 'GeoIP2-ISP',
- ip_address)
-
- def _get(self, database_type, ip_address):
- if database_type not in self.metadata().database_type:
- caller = inspect.stack()[2][3]
- raise TypeError("The %s method cannot be used with the "
- "%s database" %
- (caller, self.metadata().database_type))
- record = self._db_reader.get(ip_address)
- if record is None:
- raise geoip2.errors.AddressNotFoundError(
- "The address %s is not in the database." % ip_address)
- return record
-
- def _model_for(self, model_class, types, ip_address):
- record = self._get(types, ip_address)
- record.setdefault('traits', {})['ip_address'] = ip_address
- return model_class(record, locales=self._locales)
-
- def _flat_model_for(self, model_class, types, ip_address):
- record = self._get(types, ip_address)
- record['ip_address'] = ip_address
- return model_class(record)
-
- def metadata(self):
- """The metadata for the open database.
-
- :returns: :py:class:`maxminddb.reader.Metadata` object
- """
- return self._db_reader.metadata()
-
- def close(self):
- """Closes the GeoIP2 database."""
-
- self._db_reader.close()
diff --git a/lib/geoip2/errors.py b/lib/geoip2/errors.py
deleted file mode 100644
index 468b5858..00000000
--- a/lib/geoip2/errors.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-Errors
-======
-
-"""
-
-
-class GeoIP2Error(RuntimeError):
- """There was a generic error in GeoIP2.
-
- This class represents a generic error. It extends :py:exc:`RuntimeError`
- and does not add any additional attributes.
-
- """
-
-
-class AddressNotFoundError(GeoIP2Error):
- """The address you were looking up was not found."""
-
-
-class AuthenticationError(GeoIP2Error):
- """There was a problem authenticating the request."""
-
-
-class HTTPError(GeoIP2Error):
- """There was an error when making your HTTP request.
-
- This class represents an HTTP transport error. It extends
- :py:exc:`GeoIP2Error` and adds attributes of its own.
-
- :ivar http_status: The HTTP status code returned
- :ivar uri: The URI queried
-
- """
-
- def __init__(self, message, http_status=None, uri=None):
- super(HTTPError, self).__init__(message)
- self.http_status = http_status
- self.uri = uri
-
-
-class InvalidRequestError(GeoIP2Error):
- """The request was invalid."""
-
-
-class OutOfQueriesError(GeoIP2Error):
- """Your account is out of funds for the service queried."""
-
-
-class PermissionRequiredError(GeoIP2Error):
- """Your account does not have permission to access this service."""
diff --git a/lib/geoip2/mixins.py b/lib/geoip2/mixins.py
deleted file mode 100644
index 7fb4c275..00000000
--- a/lib/geoip2/mixins.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""This package contains utility mixins"""
-# pylint: disable=too-few-public-methods
-from abc import ABCMeta
-
-
-class SimpleEquality(object):
- """Naive __dict__ equality mixin"""
-
- __metaclass__ = ABCMeta
-
- def __eq__(self, other):
- return (isinstance(other, self.__class__) and
- self.__dict__ == other.__dict__)
-
- def __ne__(self, other):
- return not self.__eq__(other)
diff --git a/lib/geoip2/models.py b/lib/geoip2/models.py
deleted file mode 100644
index 15e951b0..00000000
--- a/lib/geoip2/models.py
+++ /dev/null
@@ -1,472 +0,0 @@
-"""
-Models
-======
-
-These classes provide models for the data returned by the GeoIP2
-web service and databases.
-
-The only difference between the City and Insights model classes is which
-fields in each record may be populated. See
-http://dev.maxmind.com/geoip/geoip2/web-services for more details.
-
-"""
-# pylint: disable=too-many-instance-attributes,too-few-public-methods
-from abc import ABCMeta
-
-import geoip2.records
-from geoip2.mixins import SimpleEquality
-
-
-class Country(SimpleEquality):
- """Model for the GeoIP2 Precision: Country and the GeoIP2 Country database.
-
- This class provides the following attributes:
-
- .. attribute:: continent
-
- Continent object for the requested IP address.
-
- :type: :py:class:`geoip2.records.Continent`
-
- .. attribute:: country
-
- Country object for the requested IP address. This record represents the
- country where MaxMind believes the IP is located.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: maxmind
-
- Information related to your MaxMind account.
-
- :type: :py:class:`geoip2.records.MaxMind`
-
- .. attribute:: registered_country
-
- The registered country object for the requested IP address. This record
- represents the country where the ISP has registered a given IP block in
- and may differ from the user's country.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: represented_country
-
- Object for the country represented by the users of the IP address
- when that country is different than the country in ``country``. For
- instance, the country represented by an overseas military base.
-
- :type: :py:class:`geoip2.records.RepresentedCountry`
-
- .. attribute:: traits
-
- Object with the traits of the requested IP address.
-
- :type: :py:class:`geoip2.records.Traits`
-
- """
-
- def __init__(self, raw_response, locales=None):
- if locales is None:
- locales = ['en']
- self._locales = locales
- self.continent = \
- geoip2.records.Continent(locales,
- **raw_response.get('continent', {}))
- self.country = \
- geoip2.records.Country(locales,
- **raw_response.get('country', {}))
- self.registered_country = \
- geoip2.records.Country(locales,
- **raw_response.get('registered_country',
- {}))
- self.represented_country \
- = geoip2.records.RepresentedCountry(locales,
- **raw_response.get(
- 'represented_country', {}))
-
- self.maxmind = \
- geoip2.records.MaxMind(**raw_response.get('maxmind', {}))
-
- self.traits = geoip2.records.Traits(**raw_response.get('traits', {}))
- self.raw = raw_response
-
- def __repr__(self):
- return '{module}.{class_name}({data}, {locales})'.format(
- module=self.__module__,
- class_name=self.__class__.__name__,
- data=self.raw,
- locales=self._locales)
-
-
-class City(Country):
- """Model for the GeoIP2 Precision: City and the GeoIP2 City database.
-
- .. attribute:: city
-
- City object for the requested IP address.
-
- :type: :py:class:`geoip2.records.City`
-
- .. attribute:: continent
-
- Continent object for the requested IP address.
-
- :type: :py:class:`geoip2.records.Continent`
-
- .. attribute:: country
-
- Country object for the requested IP address. This record represents the
- country where MaxMind believes the IP is located.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: location
-
- Location object for the requested IP address.
-
- .. attribute:: maxmind
-
- Information related to your MaxMind account.
-
- :type: :py:class:`geoip2.records.MaxMind`
-
- .. attribute:: registered_country
-
- The registered country object for the requested IP address. This record
- represents the country where the ISP has registered a given IP block in
- and may differ from the user's country.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: represented_country
-
- Object for the country represented by the users of the IP address
- when that country is different than the country in ``country``. For
- instance, the country represented by an overseas military base.
-
- :type: :py:class:`geoip2.records.RepresentedCountry`
-
- .. attribute:: subdivisions
-
- Object (tuple) representing the subdivisions of the country to which
- the location of the requested IP address belongs.
-
- :type: :py:class:`geoip2.records.Subdivisions`
-
- .. attribute:: traits
-
- Object with the traits of the requested IP address.
-
- :type: :py:class:`geoip2.records.Traits`
-
- """
-
- def __init__(self, raw_response, locales=None):
- super(City, self).__init__(raw_response, locales)
- self.city = \
- geoip2.records.City(locales, **raw_response.get('city', {}))
- self.location = \
- geoip2.records.Location(**raw_response.get('location', {}))
- self.postal = \
- geoip2.records.Postal(**raw_response.get('postal', {}))
- self.subdivisions = \
- geoip2.records.Subdivisions(locales,
- *raw_response.get('subdivisions', []))
-
-
-class Insights(City):
- """Model for the GeoIP2 Precision: Insights web service endpoint.
-
- .. attribute:: city
-
- City object for the requested IP address.
-
- :type: :py:class:`geoip2.records.City`
-
- .. attribute:: continent
-
- Continent object for the requested IP address.
-
- :type: :py:class:`geoip2.records.Continent`
-
- .. attribute:: country
-
- Country object for the requested IP address. This record represents the
- country where MaxMind believes the IP is located.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: location
-
- Location object for the requested IP address.
-
- .. attribute:: maxmind
-
- Information related to your MaxMind account.
-
- :type: :py:class:`geoip2.records.MaxMind`
-
- .. attribute:: registered_country
-
- The registered country object for the requested IP address. This record
- represents the country where the ISP has registered a given IP block in
- and may differ from the user's country.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: represented_country
-
- Object for the country represented by the users of the IP address
- when that country is different than the country in ``country``. For
- instance, the country represented by an overseas military base.
-
- :type: :py:class:`geoip2.records.RepresentedCountry`
-
- .. attribute:: subdivisions
-
- Object (tuple) representing the subdivisions of the country to which
- the location of the requested IP address belongs.
-
- :type: :py:class:`geoip2.records.Subdivisions`
-
- .. attribute:: traits
-
- Object with the traits of the requested IP address.
-
- :type: :py:class:`geoip2.records.Traits`
-
- """
-
-
-class Enterprise(City):
- """Model for the GeoIP2 Enterprise database.
-
- .. attribute:: city
-
- City object for the requested IP address.
-
- :type: :py:class:`geoip2.records.City`
-
- .. attribute:: continent
-
- Continent object for the requested IP address.
-
- :type: :py:class:`geoip2.records.Continent`
-
- .. attribute:: country
-
- Country object for the requested IP address. This record represents the
- country where MaxMind believes the IP is located.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: location
-
- Location object for the requested IP address.
-
- .. attribute:: maxmind
-
- Information related to your MaxMind account.
-
- :type: :py:class:`geoip2.records.MaxMind`
-
- .. attribute:: registered_country
-
- The registered country object for the requested IP address. This record
- represents the country where the ISP has registered a given IP block in
- and may differ from the user's country.
-
- :type: :py:class:`geoip2.records.Country`
-
- .. attribute:: represented_country
-
- Object for the country represented by the users of the IP address
- when that country is different than the country in ``country``. For
- instance, the country represented by an overseas military base.
-
- :type: :py:class:`geoip2.records.RepresentedCountry`
-
- .. attribute:: subdivisions
-
- Object (tuple) representing the subdivisions of the country to which
- the location of the requested IP address belongs.
-
- :type: :py:class:`geoip2.records.Subdivisions`
-
- .. attribute:: traits
-
- Object with the traits of the requested IP address.
-
- :type: :py:class:`geoip2.records.Traits`
-
- """
-
-
-class SimpleModel(SimpleEquality):
- """Provides basic methods for non-location models"""
-
- __metaclass__ = ABCMeta
-
- def __repr__(self):
- # pylint: disable=no-member
- return '{module}.{class_name}({data})'.format(
- module=self.__module__,
- class_name=self.__class__.__name__,
- data=str(self.raw))
-
-
-class AnonymousIP(SimpleModel):
- """Model class for the GeoIP2 Anonymous IP.
-
- This class provides the following attribute:
-
- .. attribute:: is_anonymous
-
- This is true if the IP address belongs to any sort of anonymous network.
-
- :type: bool
-
- .. attribute:: is_anonymous_vpn
-
- This is true if the IP address belongs to an anonymous VPN system.
-
- :type: bool
-
- .. attribute:: is_hosting_provider
-
- This is true if the IP address belongs to a hosting provider.
-
- :type: bool
-
- .. attribute:: is_public_proxy
-
- This is true if the IP address belongs to a public proxy.
-
- :type: bool
-
- .. attribute:: is_tor_exit_node
-
- This is true if the IP address is a Tor exit node.
-
- :type: bool
-
- .. attribute:: ip_address
-
- The IP address used in the lookup.
-
- :type: unicode
- """
-
- def __init__(self, raw):
- self.is_anonymous = raw.get('is_anonymous', False)
- self.is_anonymous_vpn = raw.get('is_anonymous_vpn', False)
- self.is_hosting_provider = raw.get('is_hosting_provider', False)
- self.is_public_proxy = raw.get('is_public_proxy', False)
- self.is_tor_exit_node = raw.get('is_tor_exit_node', False)
-
- self.ip_address = raw.get('ip_address')
- self.raw = raw
-
-
-class ConnectionType(SimpleModel):
- """Model class for the GeoIP2 Connection-Type.
-
- This class provides the following attribute:
-
- .. attribute:: connection_type
-
- The connection type may take the following values:
-
- - Dialup
- - Cable/DSL
- - Corporate
- - Cellular
-
- Additional values may be added in the future.
-
- :type: unicode
-
- .. attribute:: ip_address
-
- The IP address used in the lookup.
-
- :type: unicode
- """
-
- def __init__(self, raw):
- self.connection_type = raw.get('connection_type')
- self.ip_address = raw.get('ip_address')
- self.raw = raw
-
-
-class Domain(SimpleModel):
- """Model class for the GeoIP2 Domain.
-
- This class provides the following attribute:
-
- .. attribute:: domain
-
- The domain associated with the IP address.
-
- :type: unicode
-
- .. attribute:: ip_address
-
- The IP address used in the lookup.
-
- :type: unicode
-
- """
-
- def __init__(self, raw):
- self.domain = raw.get('domain')
- self.ip_address = raw.get('ip_address')
- self.raw = raw
-
-
-class ISP(SimpleModel):
- """Model class for the GeoIP2 ISP.
-
- This class provides the following attribute:
-
- .. attribute:: autonomous_system_number
-
- The autonomous system number associated with the IP address.
-
- :type: int
-
- .. attribute:: autonomous_system_organization
-
- The organization associated with the registered autonomous system number
- for the IP address.
-
- :type: unicode
-
- .. attribute:: isp
-
- The name of the ISP associated with the IP address.
-
- :type: unicode
-
- .. attribute:: organization
-
- The name of the organization associated with the IP address.
-
- :type: unicode
-
- .. attribute:: ip_address
-
- The IP address used in the lookup.
-
- :type: unicode
- """
-
- # pylint:disable=too-many-arguments
- def __init__(self, raw):
- self.autonomous_system_number = raw.get('autonomous_system_number')
- self.autonomous_system_organization = raw.get(
- 'autonomous_system_organization')
- self.isp = raw.get('isp')
- self.organization = raw.get('organization')
- self.ip_address = raw.get('ip_address')
- self.raw = raw
diff --git a/lib/geoip2/records.py b/lib/geoip2/records.py
deleted file mode 100644
index 7f99d121..00000000
--- a/lib/geoip2/records.py
+++ /dev/null
@@ -1,605 +0,0 @@
-"""
-
-Records
-=======
-
-"""
-
-# pylint:disable=R0903
-from abc import ABCMeta
-
-from geoip2.mixins import SimpleEquality
-
-
-class Record(SimpleEquality):
- """All records are subclasses of the abstract class ``Record``."""
-
- __metaclass__ = ABCMeta
-
- _valid_attributes = set()
-
- def __init__(self, **kwargs):
- valid_args = dict((k, kwargs.get(k)) for k in self._valid_attributes)
- self.__dict__.update(valid_args)
-
- def __setattr__(self, name, value):
- raise AttributeError("can't set attribute")
-
- def __repr__(self):
- args = ', '.join('%s=%r' % x for x in self.__dict__.items())
- return '{module}.{class_name}({data})'.format(
- module=self.__module__,
- class_name=self.__class__.__name__,
- data=args)
-
-
-class PlaceRecord(Record):
- """All records with :py:attr:`names` subclass :py:class:`PlaceRecord`."""
-
- __metaclass__ = ABCMeta
-
- def __init__(self, locales=None, **kwargs):
- if locales is None:
- locales = ['en']
- if kwargs.get('names') is None:
- kwargs['names'] = {}
- object.__setattr__(self, '_locales', locales)
- super(PlaceRecord, self).__init__(**kwargs)
-
- @property
- def name(self):
- """Dict with locale codes as keys and localized name as value."""
- # pylint:disable=E1101
- return next(
- (self.names.get(x) for x in self._locales
- if x in self.names), None)
-
-
-class City(PlaceRecord):
- """Contains data for the city record associated with an IP address.
-
- This class contains the city-level data associated with an IP address.
-
- This record is returned by ``city``, ``enterprise``, and ``insights``.
-
- Attributes:
-
- .. attribute:: confidence
-
- A value from 0-100 indicating MaxMind's
- confidence that the city is correct. This attribute is only available
- from the Insights end point and the GeoIP2 Enterprise database.
-
- :type: int
-
- .. attribute:: geoname_id
-
- The GeoName ID for the city.
-
- :type: int
-
- .. attribute:: name
-
- The name of the city based on the locales list passed to the
- constructor.
-
- :type: unicode
-
- .. attribute:: names
-
- A dictionary where the keys are locale codes
- and the values are names.
-
- :type: dict
-
- """
-
- _valid_attributes = set(['confidence', 'geoname_id', 'names'])
-
-
-class Continent(PlaceRecord):
- """Contains data for the continent record associated with an IP address.
-
- This class contains the continent-level data associated with an IP
- address.
-
- Attributes:
-
-
- .. attribute:: code
-
- A two character continent code like "NA" (North America)
- or "OC" (Oceania).
-
- :type: unicode
-
- .. attribute:: geoname_id
-
- The GeoName ID for the continent.
-
- :type: int
-
- .. attribute:: name
-
- Returns the name of the continent based on the locales list passed to
- the constructor.
-
- :type: unicode
-
- .. attribute:: names
-
- A dictionary where the keys are locale codes
- and the values are names.
-
- :type: dict
-
- """
-
- _valid_attributes = set(['code', 'geoname_id', 'names'])
-
-
-class Country(PlaceRecord):
- """Contains data for the country record associated with an IP address.
-
- This class contains the country-level data associated with an IP address.
-
- Attributes:
-
-
- .. attribute:: confidence
-
- A value from 0-100 indicating MaxMind's confidence that
- the country is correct. This attribute is only available from the
- Insights end point and the GeoIP2 Enterprise database.
-
- :type: int
-
- .. attribute:: geoname_id
-
- The GeoName ID for the country.
-
- :type: int
-
- .. attribute:: iso_code
-
- The two-character `ISO 3166-1
- `_ alpha code for the
- country.
-
- :type: unicode
-
- .. attribute:: name
-
- The name of the country based on the locales list passed to the
- constructor.
-
- :type: unicode
-
- .. attribute:: names
-
- A dictionary where the keys are locale codes and the values
- are names.
-
- :type: dict
-
- """
-
- _valid_attributes = set(['confidence', 'geoname_id', 'iso_code', 'names'])
-
-
-class RepresentedCountry(Country):
- """Contains data for the represented country associated with an IP address.
-
- This class contains the country-level data associated with an IP address
- for the IP's represented country. The represented country is the country
- represented by something like a military base.
-
- Attributes:
-
-
- .. attribute:: confidence
-
- A value from 0-100 indicating MaxMind's confidence that
- the country is correct. This attribute is only available from the
- Insights end point and the GeoIP2 Enterprise database.
-
- :type: int
-
- .. attribute:: geoname_id
-
- The GeoName ID for the country.
-
- :type: int
-
- .. attribute:: iso_code
-
- The two-character `ISO 3166-1
- `_ alpha code for the country.
-
- :type: unicode
-
- .. attribute:: name
-
- The name of the country based on the locales list passed to the
- constructor.
-
- :type: unicode
-
- .. attribute:: names
-
- A dictionary where the keys are locale codes and the values
- are names.
-
- :type: dict
-
-
- .. attribute:: type
-
- A string indicating the type of entity that is representing the
- country. Currently we only return ``military`` but this could expand to
- include other types in the future.
-
- :type: unicode
-
- """
-
- _valid_attributes = set(['confidence', 'geoname_id', 'iso_code', 'names',
- 'type'])
-
-
-class Location(Record):
- """Contains data for the location record associated with an IP address.
-
- This class contains the location data associated with an IP address.
-
- This record is returned by ``city``, ``enterprise``, and ``insights``.
-
- Attributes:
-
- .. attribute:: average_income
-
- The average income in US dollars associated with the requested IP
- address. This attribute is only available from the Insights end point.
-
- :type: int
-
- .. attribute:: accuracy_radius
-
- The radius in kilometers around the specified location where the IP
- address is likely to be.
-
- :type: int
-
- .. attribute:: latitude
-
- The approximate latitude of the location associated with the IP
- address. This value is not precise and should not be used to identify a
- particular address or household.
-
- :type: float
-
- .. attribute:: longitude
-
- The approximate longitude of the location associated with the IP
- address. This value is not precise and should not be used to identify a
- particular address or household.
-
- :type: float
-
- .. attribute:: metro_code
-
- The metro code of the location if the
- location is in the US. MaxMind returns the same metro codes as the
- `Google AdWords API
- `_.
-
- :type: int
-
- .. attribute:: population_density
-
- The estimated population per square kilometer associated with the IP
- address. This attribute is only available from the Insights end point.
-
- :type: int
-
- .. attribute:: time_zone
-
- The time zone associated with location, as specified by the `IANA Time
- Zone Database `_, e.g.,
- "America/New_York".
-
- :type: unicode
-
- """
-
- _valid_attributes = set(['average_income', 'accuracy_radius', 'latitude',
- 'longitude', 'metro_code', 'population_density',
- 'postal_code', 'postal_confidence', 'time_zone'])
-
-
-class MaxMind(Record):
- """Contains data related to your MaxMind account.
-
- Attributes:
-
- .. attribute:: queries_remaining
-
- The number of remaining queries you have
- for the end point you are calling.
-
- :type: int
-
- """
-
- _valid_attributes = set(['queries_remaining'])
-
-
-class Postal(Record):
- """Contains data for the postal record associated with an IP address.
-
- This class contains the postal data associated with an IP address.
-
- This attribute is returned by ``city``, ``enterprise``, and ``insights``.
-
- Attributes:
-
- .. attribute:: code
-
- The postal code of the location. Postal
- codes are not available for all countries. In some countries, this will
- only contain part of the postal code.
-
- :type: unicode
-
- .. attribute:: confidence
-
- A value from 0-100 indicating
- MaxMind's confidence that the postal code is correct. This attribute is
- only available from the Insights end point and the GeoIP2 Enterprise
- database.
-
- :type: int
-
- """
-
- _valid_attributes = set(['code', 'confidence'])
-
-
-class Subdivision(PlaceRecord):
- """Contains data for the subdivisions associated with an IP address.
-
- This class contains the subdivision data associated with an IP address.
-
- This attribute is returned by ``city``, ``enterprise``, and ``insights``.
-
- Attributes:
-
- .. attribute:: confidence
-
- This is a value from 0-100 indicating MaxMind's
- confidence that the subdivision is correct. This attribute is only
- available from the Insights end point and the GeoIP2 Enterprise
- database.
-
- :type: int
-
- .. attribute:: geoname_id
-
- This is a GeoName ID for the subdivision.
-
- :type: int
-
- .. attribute:: iso_code
-
- This is a string up to three characters long
- contain the subdivision portion of the `ISO 3166-2 code
- `_.
-
- :type: unicode
-
- .. attribute:: name
-
- The name of the subdivision based on the locales list passed to the
- constructor.
-
- :type: unicode
-
- .. attribute:: names
-
- A dictionary where the keys are locale codes and the
- values are names
-
- :type: dict
-
- """
-
- _valid_attributes = set(['confidence', 'geoname_id', 'iso_code', 'names'])
-
-
-class Subdivisions(tuple):
- """A tuple-like collection of subdivisions associated with an IP address.
-
- This class contains the subdivisions of the country associated with the
- IP address from largest to smallest.
-
- For instance, the response for Oxford in the United Kingdom would have
- England as the first element and Oxfordshire as the second element.
-
- This attribute is returned by ``city``, ``enterprise``, and ``insights``.
- """
-
- def __new__(cls, locales, *subdivisions):
- subdivisions = [Subdivision(locales, **x) for x in subdivisions]
- obj = super(cls, Subdivisions).__new__(cls, subdivisions)
- return obj
-
- def __init__(self, locales, *subdivisions): # pylint:disable=W0613
- self._locales = locales
- super(Subdivisions, self).__init__()
-
- @property
- def most_specific(self):
- """The most specific (smallest) subdivision available.
-
- If there are no :py:class:`Subdivision` objects for the response,
- this returns an empty :py:class:`Subdivision`.
-
- :type: :py:class:`Subdivision`
- """
- try:
- return self[-1]
- except IndexError:
- return Subdivision(self._locales)
-
-
-class Traits(Record):
- """Contains data for the traits record associated with an IP address.
-
- This class contains the traits data associated with an IP address.
-
- This class has the following attributes:
-
-
- .. attribute:: autonomous_system_number
-
- The `autonomous system
- number `_
- associated with the IP address. This attribute is only available from
- the City and Insights web service end points and the GeoIP2 Enterprise
- database.
-
- :type: int
-
- .. attribute:: autonomous_system_organization
-
- The organization associated with the registered `autonomous system
- number `_ for
- the IP address. This attribute is only available from the City and
- Insights web service end points and the GeoIP2 Enterprise database.
-
- :type: unicode
-
- .. attribute:: connection_type
-
- The connection type may take the following values:
-
- - Dialup
- - Cable/DSL
- - Corporate
- - Cellular
-
- Additional values may be added in the future.
-
- This attribute is only available in the GeoIP2 Enterprise database.
-
- :type: unicode
-
- .. attribute:: domain
-
- The second level domain associated with the
- IP address. This will be something like "example.com" or
- "example.co.uk", not "foo.example.com". This attribute is only available
- from the City and Insights web service end points and the GeoIP2
- Enterprise database.
-
- :type: unicode
-
- .. attribute:: ip_address
-
- The IP address that the data in the model
- is for. If you performed a "me" lookup against the web service, this
- will be the externally routable IP address for the system the code is
- running on. If the system is behind a NAT, this may differ from the IP
- address locally assigned to it.
-
- :type: unicode
-
- .. attribute:: is_anonymous_proxy
-
- This is true if the IP is an anonymous
- proxy. See http://dev.maxmind.com/faq/geoip#anonproxy for further
- details.
-
- :type: bool
-
- .. deprecated:: 2.2.0
- Use our our `GeoIP2 Anonymous IP database
- `_
- instead.
-
- .. attribute:: is_legitimate_proxy
-
- This attribute is true if MaxMind believes this IP address to be a
- legitimate proxy, such as an internal VPN used by a corporation. This
- attribute is only available in the GeoIP2 Enterprise database.
-
- :type: bool
-
- .. attribute:: is_satellite_provider
-
- This is true if the IP address is from a satellite provider that
- provides service to multiple countries.
-
- :type: bool
-
- .. deprecated:: 2.2.0
- Due to the increased coverage by mobile carriers, very few
- satellite providers now serve multiple countries. As a result, the
- output does not provide sufficiently relevant data for us to maintain
- it.
-
- .. attribute:: isp
-
- The name of the ISP associated with the IP address. This attribute is
- only available from the City and Insights web service end points and the
- GeoIP2 Enterprise database.
-
- :type: unicode
-
- .. attribute:: organization
-
- The name of the organization associated with the IP address. This
- attribute is only available from the City and Insights web service end
- points and the GeoIP2 Enterprise database.
-
- :type: unicode
-
- .. attribute:: user_type
-
- The user type associated with the IP
- address. This can be one of the following values:
-
- * business
- * cafe
- * cellular
- * college
- * content_delivery_network
- * dialup
- * government
- * hosting
- * library
- * military
- * residential
- * router
- * school
- * search_engine_spider
- * traveler
-
- This attribute is only available from the Insights end point and the
- GeoIP2 Enterprise database.
-
- :type: unicode
-
- """
-
- _valid_attributes = set(
- ['autonomous_system_number', 'autonomous_system_organization',
- 'connection_type', 'domain', 'is_anonymous_proxy',
- 'is_legitimate_proxy', 'is_satellite_provider', 'isp', 'ip_address',
- 'organization', 'user_type'])
-
- def __init__(self, **kwargs):
- for k in ['is_anonymous_proxy', 'is_legitimate_proxy',
- 'is_satellite_provider']:
- kwargs[k] = bool(kwargs.get(k, False))
- super(Traits, self).__init__(**kwargs)
diff --git a/lib/geoip2/webservice.py b/lib/geoip2/webservice.py
deleted file mode 100644
index c64f1b80..00000000
--- a/lib/geoip2/webservice.py
+++ /dev/null
@@ -1,219 +0,0 @@
-"""
-============================
-WebServices Client API
-============================
-
-This class provides a client API for all the GeoIP2 Precision web service end
-points. The end points are Country, City, and Insights. Each end point returns
-a different set of data about an IP address, with Country returning the least
-data and Insights the most.
-
-Each web service end point is represented by a different model class, and
-these model classes in turn contain multiple record classes. The record
-classes have attributes which contain data about the IP address.
-
-If the web service does not return a particular piece of data for an IP
-address, the associated attribute is not populated.
-
-The web service may not return any information for an entire record, in which
-case all of the attributes for that record class will be empty.
-
-SSL
----
-
-Requests to the GeoIP2 Precision web service are always made with SSL.
-
-"""
-
-import requests
-
-from requests.utils import default_user_agent
-
-import geoip2
-import geoip2.models
-
-from .compat import compat_ip_address
-
-from .errors import (AddressNotFoundError, AuthenticationError, GeoIP2Error,
- HTTPError, InvalidRequestError, OutOfQueriesError,
- PermissionRequiredError)
-
-
-class Client(object):
- """Creates a new client object.
-
- It accepts the following required arguments:
-
- :param user_id: Your MaxMind User ID.
- :param license_key: Your MaxMind license key.
-
- Go to https://www.maxmind.com/en/my_license_key to see your MaxMind
- User ID and license key.
-
- The following keyword arguments are also accepted:
-
- :param host: The hostname to make a request against. This defaults to
- "geoip.maxmind.com". In most cases, you should not need to set this
- explicitly.
- :param locales: This is list of locale codes. This argument will be
- passed on to record classes to use when their name properties are
- called. The default value is ['en'].
-
- The order of the locales is significant. When a record class has
- multiple names (country, city, etc.), its name property will return
- the name in the first locale that has one.
-
- Note that the only locale which is always present in the GeoIP2
- data is "en". If you do not include this locale, the name property
- may end up returning None even when the record has an English name.
-
- Currently, the valid locale codes are:
-
- * de -- German
- * en -- English names may still include accented characters if that is
- the accepted spelling in English. In other words, English does not
- mean ASCII.
- * es -- Spanish
- * fr -- French
- * ja -- Japanese
- * pt-BR -- Brazilian Portuguese
- * ru -- Russian
- * zh-CN -- Simplified Chinese.
-
- """
-
- def __init__(self,
- user_id,
- license_key,
- host='geoip.maxmind.com',
- locales=None,
- timeout=None):
- """Construct a Client."""
- # pylint: disable=too-many-arguments
- if locales is None:
- locales = ['en']
- self._locales = locales
- self._user_id = user_id
- self._license_key = license_key
- self._base_uri = 'https://%s/geoip/v2.1' % host
- self._timeout = timeout
-
- def city(self, ip_address='me'):
- """Call GeoIP2 Precision City endpoint with the specified IP.
-
- :param ip_address: IPv4 or IPv6 address as a string. If no
- address is provided, the address that the web service is
- called from will be used.
-
- :returns: :py:class:`geoip2.models.City` object
-
- """
- return self._response_for('city', geoip2.models.City, ip_address)
-
- def country(self, ip_address='me'):
- """Call the GeoIP2 Country endpoint with the specified IP.
-
- :param ip_address: IPv4 or IPv6 address as a string. If no address
- is provided, the address that the web service is called from will
- be used.
-
- :returns: :py:class:`geoip2.models.Country` object
-
- """
- return self._response_for('country', geoip2.models.Country, ip_address)
-
- def insights(self, ip_address='me'):
- """Call the GeoIP2 Precision: Insights endpoint with the specified IP.
-
- :param ip_address: IPv4 or IPv6 address as a string. If no address
- is provided, the address that the web service is called from will
- be used.
-
- :returns: :py:class:`geoip2.models.Insights` object
-
- """
- return self._response_for('insights', geoip2.models.Insights,
- ip_address)
-
- def _response_for(self, path, model_class, ip_address):
- if ip_address != 'me':
- ip_address = str(compat_ip_address(ip_address))
- uri = '/'.join([self._base_uri, path, ip_address])
- response = requests.get(uri,
- auth=(self._user_id, self._license_key),
- headers={'Accept': 'application/json',
- 'User-Agent': self._user_agent()},
- timeout=self._timeout)
- if response.status_code == 200:
- body = self._handle_success(response, uri)
- return model_class(body, locales=self._locales)
- else:
- self._handle_error(response, uri)
-
- def _user_agent(self):
- return 'GeoIP2 Python Client v%s (%s)' % (geoip2.__version__,
- default_user_agent())
-
- def _handle_success(self, response, uri):
- try:
- return response.json()
- except ValueError as ex:
- raise GeoIP2Error('Received a 200 response for %(uri)s'
- ' but could not decode the response as '
- 'JSON: ' % locals() + ', '.join(ex.args), 200,
- uri)
-
- def _handle_error(self, response, uri):
- status = response.status_code
-
- if 400 <= status < 500:
- self._handle_4xx_status(response, status, uri)
- elif 500 <= status < 600:
- self._handle_5xx_status(status, uri)
- else:
- self._handle_non_200_status(status, uri)
-
- def _handle_4xx_status(self, response, status, uri):
- if not response.content:
- raise HTTPError('Received a %(status)i error for %(uri)s '
- 'with no body.' % locals(), status, uri)
- elif response.headers['Content-Type'].find('json') == -1:
- raise HTTPError('Received a %i for %s with the following '
- 'body: %s' % (status, uri, response.content),
- status, uri)
- try:
- body = response.json()
- except ValueError as ex:
- raise HTTPError(
- 'Received a %(status)i error for %(uri)s but it did'
- ' not include the expected JSON body: ' % locals() +
- ', '.join(ex.args), status, uri)
- else:
- if 'code' in body and 'error' in body:
- self._handle_web_service_error(
- body.get('error'), body.get('code'), status, uri)
- else:
- raise HTTPError(
- 'Response contains JSON but it does not specify '
- 'code or error keys', status, uri)
-
- def _handle_web_service_error(self, message, code, status, uri):
- if code in ('IP_ADDRESS_NOT_FOUND', 'IP_ADDRESS_RESERVED'):
- raise AddressNotFoundError(message)
- elif code in ('AUTHORIZATION_INVALID', 'LICENSE_KEY_REQUIRED',
- 'USER_ID_REQUIRED', 'USER_ID_UNKNOWN'):
- raise AuthenticationError(message)
- elif code in ('INSUFFICIENT_FUNDS', 'OUT_OF_QUERIES'):
- raise OutOfQueriesError(message)
- elif code == 'PERMISSION_REQUIRED':
- raise PermissionRequiredError(message)
-
- raise InvalidRequestError(message, code, status, uri)
-
- def _handle_5xx_status(self, status, uri):
- raise HTTPError('Received a server error (%(status)i) for '
- '%(uri)s' % locals(), status, uri)
-
- def _handle_non_200_status(self, status, uri):
- raise HTTPError('Received a very surprising HTTP status '
- '(%(status)i) for %(uri)s' % locals(), status, uri)
diff --git a/lib/maxminddb/__init__.py b/lib/maxminddb/__init__.py
deleted file mode 100644
index 7c6008b3..00000000
--- a/lib/maxminddb/__init__.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# pylint:disable=C0111
-import os
-
-import maxminddb.reader
-
-try:
- import maxminddb.extension
-except ImportError:
- maxminddb.extension = None
-
-from maxminddb.const import (MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, MODE_FILE,
- MODE_MEMORY)
-from maxminddb.decoder import InvalidDatabaseError
-
-
-def open_database(database, mode=MODE_AUTO):
- """Open a Maxmind DB database
-
- Arguments:
- database -- A path to a valid MaxMind DB file such as a GeoIP2
- database file.
- mode -- mode to open the database with. Valid mode are:
- * MODE_MMAP_EXT - use the C extension with memory map.
- * MODE_MMAP - read from memory map. Pure Python.
- * MODE_FILE - read database as standard file. Pure Python.
- * MODE_MEMORY - load database into memory. Pure Python.
- * MODE_AUTO - tries MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that
- order. Default mode.
- """
- if (mode == MODE_AUTO and maxminddb.extension and
- hasattr(maxminddb.extension, 'Reader')) or mode == MODE_MMAP_EXT:
- return maxminddb.extension.Reader(database)
- elif mode in (MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY):
- return maxminddb.reader.Reader(database, mode)
- raise ValueError('Unsupported open mode: {0}'.format(mode))
-
-
-def Reader(database): # pylint: disable=invalid-name
- """This exists for backwards compatibility. Use open_database instead"""
- return open_database(database)
-
-__title__ = 'maxminddb'
-__version__ = '1.2.1'
-__author__ = 'Gregory Oschwald'
-__license__ = 'Apache License, Version 2.0'
-__copyright__ = 'Copyright 2014 Maxmind, Inc.'
diff --git a/lib/maxminddb/compat.py b/lib/maxminddb/compat.py
deleted file mode 100644
index 8e2a81c5..00000000
--- a/lib/maxminddb/compat.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import sys
-
-import ipaddress
-
-# pylint: skip-file
-
-if sys.version_info[0] == 2:
- def compat_ip_address(address):
- if isinstance(address, bytes):
- address = address.decode()
- return ipaddress.ip_address(address)
-
- int_from_byte = ord
-
- FileNotFoundError = IOError
-
- def int_from_bytes(b):
- if b:
- return int(b.encode("hex"), 16)
- return 0
-
- byte_from_int = chr
-else:
- def compat_ip_address(address):
- return ipaddress.ip_address(address)
-
- int_from_byte = lambda x: x
-
- FileNotFoundError = FileNotFoundError
-
- int_from_bytes = lambda x: int.from_bytes(x, 'big')
-
- byte_from_int = lambda x: bytes([x])
diff --git a/lib/maxminddb/const.py b/lib/maxminddb/const.py
deleted file mode 100644
index 59ea84b6..00000000
--- a/lib/maxminddb/const.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""Constants used in the API"""
-
-MODE_AUTO = 0
-MODE_MMAP_EXT = 1
-MODE_MMAP = 2
-MODE_FILE = 4
-MODE_MEMORY = 8
diff --git a/lib/maxminddb/decoder.py b/lib/maxminddb/decoder.py
deleted file mode 100644
index e8f223a8..00000000
--- a/lib/maxminddb/decoder.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""
-maxminddb.decoder
-~~~~~~~~~~~~~~~~~
-
-This package contains code for decoding the MaxMind DB data section.
-
-"""
-from __future__ import unicode_literals
-
-import struct
-
-from maxminddb.compat import byte_from_int, int_from_bytes
-from maxminddb.errors import InvalidDatabaseError
-
-
-class Decoder(object): # pylint: disable=too-few-public-methods
-
- """Decoder for the data section of the MaxMind DB"""
-
- def __init__(self, database_buffer, pointer_base=0, pointer_test=False):
- """Created a Decoder for a MaxMind DB
-
- Arguments:
- database_buffer -- an mmap'd MaxMind DB file.
- pointer_base -- the base number to use when decoding a pointer
- pointer_test -- used for internal unit testing of pointer code
- """
- self._pointer_test = pointer_test
- self._buffer = database_buffer
- self._pointer_base = pointer_base
-
- def _decode_array(self, size, offset):
- array = []
- for _ in range(size):
- (value, offset) = self.decode(offset)
- array.append(value)
- return array, offset
-
- def _decode_boolean(self, size, offset):
- return size != 0, offset
-
- def _decode_bytes(self, size, offset):
- new_offset = offset + size
- return self._buffer[offset:new_offset], new_offset
-
- # pylint: disable=no-self-argument
- # |-> I am open to better ways of doing this as long as it doesn't involve
- # lots of code duplication.
- def _decode_packed_type(type_code, type_size, pad=False):
- # pylint: disable=protected-access, missing-docstring
- def unpack_type(self, size, offset):
- if not pad:
- self._verify_size(size, type_size)
- new_offset = offset + type_size
- packed_bytes = self._buffer[offset:new_offset]
- if pad:
- packed_bytes = packed_bytes.rjust(type_size, b'\x00')
- (value,) = struct.unpack(type_code, packed_bytes)
- return value, new_offset
- return unpack_type
-
- def _decode_map(self, size, offset):
- container = {}
- for _ in range(size):
- (key, offset) = self.decode(offset)
- (value, offset) = self.decode(offset)
- container[key] = value
- return container, offset
-
- _pointer_value_offset = {
- 1: 0,
- 2: 2048,
- 3: 526336,
- 4: 0,
- }
-
- def _decode_pointer(self, size, offset):
- pointer_size = ((size >> 3) & 0x3) + 1
- new_offset = offset + pointer_size
- pointer_bytes = self._buffer[offset:new_offset]
- packed = pointer_bytes if pointer_size == 4 else struct.pack(
- b'!c', byte_from_int(size & 0x7)) + pointer_bytes
- unpacked = int_from_bytes(packed)
- pointer = unpacked + self._pointer_base + \
- self._pointer_value_offset[pointer_size]
- if self._pointer_test:
- return pointer, new_offset
- (value, _) = self.decode(pointer)
- return value, new_offset
-
- def _decode_uint(self, size, offset):
- new_offset = offset + size
- uint_bytes = self._buffer[offset:new_offset]
- return int_from_bytes(uint_bytes), new_offset
-
- def _decode_utf8_string(self, size, offset):
- new_offset = offset + size
- return self._buffer[offset:new_offset].decode('utf-8'), new_offset
-
- _type_decoder = {
- 1: _decode_pointer,
- 2: _decode_utf8_string,
- 3: _decode_packed_type(b'!d', 8), # double,
- 4: _decode_bytes,
- 5: _decode_uint, # uint16
- 6: _decode_uint, # uint32
- 7: _decode_map,
- 8: _decode_packed_type(b'!i', 4, pad=True), # int32
- 9: _decode_uint, # uint64
- 10: _decode_uint, # uint128
- 11: _decode_array,
- 14: _decode_boolean,
- 15: _decode_packed_type(b'!f', 4), # float,
- }
-
- def decode(self, offset):
- """Decode a section of the data section starting at offset
-
- Arguments:
- offset -- the location of the data structure to decode
- """
- new_offset = offset + 1
- (ctrl_byte,) = struct.unpack(b'!B', self._buffer[offset:new_offset])
- type_num = ctrl_byte >> 5
- # Extended type
- if not type_num:
- (type_num, new_offset) = self._read_extended(new_offset)
-
- if type_num not in self._type_decoder:
- raise InvalidDatabaseError('Unexpected type number ({type}) '
- 'encountered'.format(type=type_num))
-
- (size, new_offset) = self._size_from_ctrl_byte(
- ctrl_byte, new_offset, type_num)
- return self._type_decoder[type_num](self, size, new_offset)
-
- def _read_extended(self, offset):
- (next_byte,) = struct.unpack(b'!B', self._buffer[offset:offset + 1])
- type_num = next_byte + 7
- if type_num < 7:
- raise InvalidDatabaseError(
- 'Something went horribly wrong in the decoder. An '
- 'extended type resolved to a type number < 8 '
- '({type})'.format(type=type_num))
- return type_num, offset + 1
-
- def _verify_size(self, expected, actual):
- if expected != actual:
- raise InvalidDatabaseError(
- 'The MaxMind DB file\'s data section contains bad data '
- '(unknown data type or corrupt data)'
- )
-
- def _size_from_ctrl_byte(self, ctrl_byte, offset, type_num):
- size = ctrl_byte & 0x1f
- if type_num == 1:
- return size, offset
- bytes_to_read = 0 if size < 29 else size - 28
-
- new_offset = offset + bytes_to_read
- size_bytes = self._buffer[offset:new_offset]
-
- # Using unpack rather than int_from_bytes as it is about 200 lookups
- # per second faster here.
- if size == 29:
- size = 29 + struct.unpack(b'!B', size_bytes)[0]
- elif size == 30:
- size = 285 + struct.unpack(b'!H', size_bytes)[0]
- elif size > 30:
- size = struct.unpack(
- b'!I', size_bytes.rjust(4, b'\x00'))[0] + 65821
-
- return size, new_offset
diff --git a/lib/maxminddb/errors.py b/lib/maxminddb/errors.py
deleted file mode 100644
index f04ff028..00000000
--- a/lib/maxminddb/errors.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-maxminddb.errors
-~~~~~~~~~~~~~~~~
-
-This module contains custom errors for the MaxMind DB reader
-"""
-
-
-class InvalidDatabaseError(RuntimeError):
-
- """This error is thrown when unexpected data is found in the database."""
diff --git a/lib/maxminddb/extension/maxminddb.c b/lib/maxminddb/extension/maxminddb.c
deleted file mode 100644
index 9e4d45e2..00000000
--- a/lib/maxminddb/extension/maxminddb.c
+++ /dev/null
@@ -1,570 +0,0 @@
-#include
-#include
-#include "structmember.h"
-
-#define __STDC_FORMAT_MACROS
-#include
-
-static PyTypeObject Reader_Type;
-static PyTypeObject Metadata_Type;
-static PyObject *MaxMindDB_error;
-
-typedef struct {
- PyObject_HEAD /* no semicolon */
- MMDB_s *mmdb;
-} Reader_obj;
-
-typedef struct {
- PyObject_HEAD /* no semicolon */
- PyObject *binary_format_major_version;
- PyObject *binary_format_minor_version;
- PyObject *build_epoch;
- PyObject *database_type;
- PyObject *description;
- PyObject *ip_version;
- PyObject *languages;
- PyObject *node_count;
- PyObject *record_size;
-} Metadata_obj;
-
-static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list);
-static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list);
-static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list);
-static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list);
-
-#if PY_MAJOR_VERSION >= 3
- #define MOD_INIT(name) PyMODINIT_FUNC PyInit_ ## name(void)
- #define RETURN_MOD_INIT(m) return (m)
- #define FILE_NOT_FOUND_ERROR PyExc_FileNotFoundError
-#else
- #define MOD_INIT(name) PyMODINIT_FUNC init ## name(void)
- #define RETURN_MOD_INIT(m) return
- #define PyInt_FromLong PyLong_FromLong
- #define FILE_NOT_FOUND_ERROR PyExc_IOError
-#endif
-
-#ifdef __GNUC__
- # define UNUSED(x) UNUSED_ ## x __attribute__((__unused__))
-#else
- # define UNUSED(x) UNUSED_ ## x
-#endif
-
-static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds)
-{
- char *filename;
- int mode = 0;
-
- static char *kwlist[] = {"database", "mode", NULL};
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|i", kwlist, &filename, &mode)) {
- return -1;
- }
-
- if (mode != 0 && mode != 1) {
- PyErr_Format(PyExc_ValueError, "Unsupported open mode (%i). Only "
- "MODE_AUTO and MODE_MMAP_EXT are supported by this extension.",
- mode);
- return -1;
- }
-
- if (0 != access(filename, R_OK)) {
- PyErr_Format(FILE_NOT_FOUND_ERROR,
- "No such file or directory: '%s'",
- filename);
- return -1;
- }
-
- MMDB_s *mmdb = (MMDB_s *)malloc(sizeof(MMDB_s));
- if (NULL == mmdb) {
- PyErr_NoMemory();
- return -1;
- }
-
- Reader_obj *mmdb_obj = (Reader_obj *)self;
- if (!mmdb_obj) {
- free(mmdb);
- PyErr_NoMemory();
- return -1;
- }
-
- uint16_t status = MMDB_open(filename, MMDB_MODE_MMAP, mmdb);
-
- if (MMDB_SUCCESS != status) {
- free(mmdb);
- PyErr_Format(
- MaxMindDB_error,
- "Error opening database file (%s). Is this a valid MaxMind DB file?",
- filename
- );
- return -1;
- }
-
- mmdb_obj->mmdb = mmdb;
- return 0;
-}
-
-static PyObject *Reader_get(PyObject *self, PyObject *args)
-{
- char *ip_address = NULL;
-
- Reader_obj *mmdb_obj = (Reader_obj *)self;
- if (!PyArg_ParseTuple(args, "s", &ip_address)) {
- return NULL;
- }
-
- MMDB_s *mmdb = mmdb_obj->mmdb;
-
- if (NULL == mmdb) {
- PyErr_SetString(PyExc_ValueError,
- "Attempt to read from a closed MaxMind DB.");
- return NULL;
- }
-
- int gai_error = 0;
- int mmdb_error = MMDB_SUCCESS;
- MMDB_lookup_result_s result =
- MMDB_lookup_string(mmdb, ip_address, &gai_error,
- &mmdb_error);
-
- if (0 != gai_error) {
- PyErr_Format(PyExc_ValueError,
- "'%s' does not appear to be an IPv4 or IPv6 address.",
- ip_address);
- return NULL;
- }
-
- if (MMDB_SUCCESS != mmdb_error) {
- PyObject *exception;
- if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
- exception = PyExc_ValueError;
- } else {
- exception = MaxMindDB_error;
- }
- PyErr_Format(exception, "Error looking up %s. %s",
- ip_address, MMDB_strerror(mmdb_error));
- return NULL;
- }
-
- if (!result.found_entry) {
- Py_RETURN_NONE;
- }
-
- MMDB_entry_data_list_s *entry_data_list = NULL;
- int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
- if (MMDB_SUCCESS != status) {
- PyErr_Format(MaxMindDB_error,
- "Error while looking up data for %s. %s",
- ip_address, MMDB_strerror(status));
- MMDB_free_entry_data_list(entry_data_list);
- return NULL;
- }
-
- MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
- PyObject *py_obj = from_entry_data_list(&entry_data_list);
- MMDB_free_entry_data_list(original_entry_data_list);
- return py_obj;
-}
-
-static PyObject *Reader_metadata(PyObject *self, PyObject *UNUSED(args))
-{
- Reader_obj *mmdb_obj = (Reader_obj *)self;
-
- if (NULL == mmdb_obj->mmdb) {
- PyErr_SetString(PyExc_IOError,
- "Attempt to read from a closed MaxMind DB.");
- return NULL;
- }
-
- MMDB_entry_data_list_s *entry_data_list;
- MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
- MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
-
- PyObject *metadata_dict = from_entry_data_list(&entry_data_list);
- MMDB_free_entry_data_list(original_entry_data_list);
- if (NULL == metadata_dict || !PyDict_Check(metadata_dict)) {
- PyErr_SetString(MaxMindDB_error,
- "Error decoding metadata.");
- return NULL;
- }
-
- PyObject *args = PyTuple_New(0);
- if (NULL == args) {
- Py_DECREF(metadata_dict);
- return NULL;
- }
-
- PyObject *metadata = PyObject_Call((PyObject *)&Metadata_Type, args,
- metadata_dict);
-
- Py_DECREF(metadata_dict);
- return metadata;
-}
-
-static PyObject *Reader_close(PyObject *self, PyObject *UNUSED(args))
-{
- Reader_obj *mmdb_obj = (Reader_obj *)self;
-
- if (NULL != mmdb_obj->mmdb) {
- MMDB_close(mmdb_obj->mmdb);
- free(mmdb_obj->mmdb);
- mmdb_obj->mmdb = NULL;
- }
-
- Py_RETURN_NONE;
-}
-
-static void Reader_dealloc(PyObject *self)
-{
- Reader_obj *obj = (Reader_obj *)self;
- if (NULL != obj->mmdb) {
- Reader_close(self, NULL);
- }
-
- PyObject_Del(self);
-}
-
-static int Metadata_init(PyObject *self, PyObject *args, PyObject *kwds)
-{
-
- PyObject
- *binary_format_major_version,
- *binary_format_minor_version,
- *build_epoch,
- *database_type,
- *description,
- *ip_version,
- *languages,
- *node_count,
- *record_size;
-
- static char *kwlist[] = {
- "binary_format_major_version",
- "binary_format_minor_version",
- "build_epoch",
- "database_type",
- "description",
- "ip_version",
- "languages",
- "node_count",
- "record_size",
- NULL
- };
-
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOOOO", kwlist,
- &binary_format_major_version,
- &binary_format_minor_version,
- &build_epoch,
- &database_type,
- &description,
- &ip_version,
- &languages,
- &node_count,
- &record_size)) {
- return -1;
- }
-
- Metadata_obj *obj = (Metadata_obj *)self;
-
- obj->binary_format_major_version = binary_format_major_version;
- obj->binary_format_minor_version = binary_format_minor_version;
- obj->build_epoch = build_epoch;
- obj->database_type = database_type;
- obj->description = description;
- obj->ip_version = ip_version;
- obj->languages = languages;
- obj->node_count = node_count;
- obj->record_size = record_size;
-
- Py_INCREF(obj->binary_format_major_version);
- Py_INCREF(obj->binary_format_minor_version);
- Py_INCREF(obj->build_epoch);
- Py_INCREF(obj->database_type);
- Py_INCREF(obj->description);
- Py_INCREF(obj->ip_version);
- Py_INCREF(obj->languages);
- Py_INCREF(obj->node_count);
- Py_INCREF(obj->record_size);
-
- return 0;
-}
-
-static void Metadata_dealloc(PyObject *self)
-{
- Metadata_obj *obj = (Metadata_obj *)self;
- Py_DECREF(obj->binary_format_major_version);
- Py_DECREF(obj->binary_format_minor_version);
- Py_DECREF(obj->build_epoch);
- Py_DECREF(obj->database_type);
- Py_DECREF(obj->description);
- Py_DECREF(obj->ip_version);
- Py_DECREF(obj->languages);
- Py_DECREF(obj->node_count);
- Py_DECREF(obj->record_size);
- PyObject_Del(self);
-}
-
-static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list)
-{
- if (NULL == entry_data_list || NULL == *entry_data_list) {
- PyErr_SetString(
- MaxMindDB_error,
- "Error while looking up data. Your database may be corrupt or you have found a bug in libmaxminddb."
- );
- return NULL;
- }
-
- switch ((*entry_data_list)->entry_data.type) {
- case MMDB_DATA_TYPE_MAP:
- return from_map(entry_data_list);
- case MMDB_DATA_TYPE_ARRAY:
- return from_array(entry_data_list);
- case MMDB_DATA_TYPE_UTF8_STRING:
- return PyUnicode_FromStringAndSize(
- (*entry_data_list)->entry_data.utf8_string,
- (*entry_data_list)->entry_data.data_size
- );
- case MMDB_DATA_TYPE_BYTES:
- return PyByteArray_FromStringAndSize(
- (const char *)(*entry_data_list)->entry_data.bytes,
- (Py_ssize_t)(*entry_data_list)->entry_data.data_size);
- case MMDB_DATA_TYPE_DOUBLE:
- return PyFloat_FromDouble((*entry_data_list)->entry_data.double_value);
- case MMDB_DATA_TYPE_FLOAT:
- return PyFloat_FromDouble((*entry_data_list)->entry_data.float_value);
- case MMDB_DATA_TYPE_UINT16:
- return PyLong_FromLong( (*entry_data_list)->entry_data.uint16);
- case MMDB_DATA_TYPE_UINT32:
- return PyLong_FromLong((*entry_data_list)->entry_data.uint32);
- case MMDB_DATA_TYPE_BOOLEAN:
- return PyBool_FromLong((*entry_data_list)->entry_data.boolean);
- case MMDB_DATA_TYPE_UINT64:
- return PyLong_FromUnsignedLongLong(
- (*entry_data_list)->entry_data.uint64);
- case MMDB_DATA_TYPE_UINT128:
- return from_uint128(*entry_data_list);
- case MMDB_DATA_TYPE_INT32:
- return PyLong_FromLong((*entry_data_list)->entry_data.int32);
- default:
- PyErr_Format(MaxMindDB_error,
- "Invalid data type arguments: %d",
- (*entry_data_list)->entry_data.type);
- return NULL;
- }
- return NULL;
-}
-
-static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list)
-{
- PyObject *py_obj = PyDict_New();
- if (NULL == py_obj) {
- PyErr_NoMemory();
- return NULL;
- }
-
- const uint32_t map_size = (*entry_data_list)->entry_data.data_size;
-
- uint i;
- // entry_data_list cannot start out NULL (see from_entry_data_list). We
- // check it in the loop because it may become NULL.
- // coverity[check_after_deref]
- for (i = 0; i < map_size && entry_data_list; i++) {
- *entry_data_list = (*entry_data_list)->next;
-
- PyObject *key = PyUnicode_FromStringAndSize(
- (char *)(*entry_data_list)->entry_data.utf8_string,
- (*entry_data_list)->entry_data.data_size
- );
-
- *entry_data_list = (*entry_data_list)->next;
-
- PyObject *value = from_entry_data_list(entry_data_list);
- if (NULL == value) {
- Py_DECREF(key);
- Py_DECREF(py_obj);
- return NULL;
- }
- PyDict_SetItem(py_obj, key, value);
- Py_DECREF(value);
- Py_DECREF(key);
- }
-
- return py_obj;
-}
-
-static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list)
-{
- const uint32_t size = (*entry_data_list)->entry_data.data_size;
-
- PyObject *py_obj = PyList_New(size);
- if (NULL == py_obj) {
- PyErr_NoMemory();
- return NULL;
- }
-
- uint i;
- // entry_data_list cannot start out NULL (see from_entry_data_list). We
- // check it in the loop because it may become NULL.
- // coverity[check_after_deref]
- for (i = 0; i < size && entry_data_list; i++) {
- *entry_data_list = (*entry_data_list)->next;
- PyObject *value = from_entry_data_list(entry_data_list);
- if (NULL == value) {
- Py_DECREF(py_obj);
- return NULL;
- }
- // PyList_SetItem 'steals' the reference
- PyList_SetItem(py_obj, i, value);
- }
- return py_obj;
-}
-
-static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list)
-{
- uint64_t high = 0;
- uint64_t low = 0;
-#if MMDB_UINT128_IS_BYTE_ARRAY
- int i;
- for (i = 0; i < 8; i++) {
- high = (high << 8) | entry_data_list->entry_data.uint128[i];
- }
-
- for (i = 8; i < 16; i++) {
- low = (low << 8) | entry_data_list->entry_data.uint128[i];
- }
-#else
- high = entry_data_list->entry_data.uint128 >> 64;
- low = (uint64_t)entry_data_list->entry_data.uint128;
-#endif
-
- char *num_str = malloc(33);
- if (NULL == num_str) {
- PyErr_NoMemory();
- return NULL;
- }
-
- snprintf(num_str, 33, "%016" PRIX64 "%016" PRIX64, high, low);
-
- PyObject *py_obj = PyLong_FromString(num_str, NULL, 16);
-
- free(num_str);
- return py_obj;
-}
-
-static PyMethodDef Reader_methods[] = {
- { "get", Reader_get, METH_VARARGS,
- "Get record for IP address" },
- { "metadata", Reader_metadata, METH_NOARGS,
- "Returns metadata object for database" },
- { "close", Reader_close, METH_NOARGS, "Closes database"},
- { NULL, NULL, 0, NULL }
-};
-
-static PyTypeObject Reader_Type = {
- PyVarObject_HEAD_INIT(NULL, 0)
- .tp_basicsize = sizeof(Reader_obj),
- .tp_dealloc = Reader_dealloc,
- .tp_doc = "Reader object",
- .tp_flags = Py_TPFLAGS_DEFAULT,
- .tp_methods = Reader_methods,
- .tp_name = "Reader",
- .tp_init = Reader_init,
-};
-
-static PyMethodDef Metadata_methods[] = {
- { NULL, NULL, 0, NULL }
-};
-
-/* *INDENT-OFF* */
-static PyMemberDef Metadata_members[] = {
- { "binary_format_major_version", T_OBJECT, offsetof(
- Metadata_obj, binary_format_major_version), READONLY, NULL },
- { "binary_format_minor_version", T_OBJECT, offsetof(
- Metadata_obj, binary_format_minor_version), READONLY, NULL },
- { "build_epoch", T_OBJECT, offsetof(Metadata_obj, build_epoch),
- READONLY, NULL },
- { "database_type", T_OBJECT, offsetof(Metadata_obj, database_type),
- READONLY, NULL },
- { "description", T_OBJECT, offsetof(Metadata_obj, description),
- READONLY, NULL },
- { "ip_version", T_OBJECT, offsetof(Metadata_obj, ip_version),
- READONLY, NULL },
- { "languages", T_OBJECT, offsetof(Metadata_obj, languages), READONLY,
- NULL },
- { "node_count", T_OBJECT, offsetof(Metadata_obj, node_count),
- READONLY, NULL },
- { "record_size", T_OBJECT, offsetof(Metadata_obj, record_size),
- READONLY, NULL },
- { NULL, 0, 0, 0, NULL }
-};
-/* *INDENT-ON* */
-
-static PyTypeObject Metadata_Type = {
- PyVarObject_HEAD_INIT(NULL, 0)
- .tp_basicsize = sizeof(Metadata_obj),
- .tp_dealloc = Metadata_dealloc,
- .tp_doc = "Metadata object",
- .tp_flags = Py_TPFLAGS_DEFAULT,
- .tp_members = Metadata_members,
- .tp_methods = Metadata_methods,
- .tp_name = "Metadata",
- .tp_init = Metadata_init
-};
-
-static PyMethodDef MaxMindDB_methods[] = {
- { NULL, NULL, 0, NULL }
-};
-
-
-#if PY_MAJOR_VERSION >= 3
-static struct PyModuleDef MaxMindDB_module = {
- PyModuleDef_HEAD_INIT,
- .m_name = "extension",
- .m_doc = "This is a C extension to read MaxMind DB file format",
- .m_methods = MaxMindDB_methods,
-};
-#endif
-
-MOD_INIT(extension){
- PyObject *m;
-
-#if PY_MAJOR_VERSION >= 3
- m = PyModule_Create(&MaxMindDB_module);
-#else
- m = Py_InitModule("extension", MaxMindDB_methods);
-#endif
-
- if (!m) {
- RETURN_MOD_INIT(NULL);
- }
-
- Reader_Type.tp_new = PyType_GenericNew;
- if (PyType_Ready(&Reader_Type)) {
- RETURN_MOD_INIT(NULL);
- }
- Py_INCREF(&Reader_Type);
- PyModule_AddObject(m, "Reader", (PyObject *)&Reader_Type);
-
- Metadata_Type.tp_new = PyType_GenericNew;
- if (PyType_Ready(&Metadata_Type)) {
- RETURN_MOD_INIT(NULL);
- }
- PyModule_AddObject(m, "extension", (PyObject *)&Metadata_Type);
-
- PyObject* error_mod = PyImport_ImportModule("maxminddb.errors");
- if (error_mod == NULL) {
- RETURN_MOD_INIT(NULL);
- }
-
- MaxMindDB_error = PyObject_GetAttrString(error_mod, "InvalidDatabaseError");
- Py_DECREF(error_mod);
-
- if (MaxMindDB_error == NULL) {
- RETURN_MOD_INIT(NULL);
- }
-
- Py_INCREF(MaxMindDB_error);
-
- /* We primarily add it to the module for backwards compatibility */
- PyModule_AddObject(m, "InvalidDatabaseError", MaxMindDB_error);
-
- RETURN_MOD_INIT(m);
-}
diff --git a/lib/maxminddb/file.py b/lib/maxminddb/file.py
deleted file mode 100644
index 2e01e756..00000000
--- a/lib/maxminddb/file.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""For internal use only. It provides a slice-like file reader."""
-
-import os
-
-try:
- # pylint: disable=no-name-in-module
- from multiprocessing import Lock
-except ImportError:
- from threading import Lock
-
-
-class FileBuffer(object):
-
- """A slice-able file reader"""
-
- def __init__(self, database):
- self._handle = open(database, 'rb')
- self._size = os.fstat(self._handle.fileno()).st_size
- if not hasattr(os, 'pread'):
- self._lock = Lock()
-
- def __getitem__(self, key):
- if isinstance(key, slice):
- return self._read(key.stop - key.start, key.start)
- elif isinstance(key, int):
- return self._read(1, key)
- else:
- raise TypeError("Invalid argument type.")
-
- def rfind(self, needle, start):
- """Reverse find needle from start"""
- pos = self._read(self._size - start - 1, start).rfind(needle)
- if pos == -1:
- return pos
- return start + pos
-
- def size(self):
- """Size of file"""
- return self._size
-
- def close(self):
- """Close file"""
- self._handle.close()
-
- if hasattr(os, 'pread'):
-
- def _read(self, buffersize, offset):
- """read that uses pread"""
- # pylint: disable=no-member
- return os.pread(self._handle.fileno(), buffersize, offset)
-
- else:
-
- def _read(self, buffersize, offset):
- """read with a lock
-
- This lock is necessary as after a fork, the different processes
- will share the same file table entry, even if we dup the fd, and
- as such the same offsets. There does not appear to be a way to
- duplicate the file table entry and we cannot re-open based on the
- original path as that file may have replaced with another or
- unlinked.
- """
- with self._lock:
- self._handle.seek(offset)
- return self._handle.read(buffersize)
diff --git a/lib/maxminddb/reader.py b/lib/maxminddb/reader.py
deleted file mode 100644
index b45f31e2..00000000
--- a/lib/maxminddb/reader.py
+++ /dev/null
@@ -1,223 +0,0 @@
-"""
-maxminddb.reader
-~~~~~~~~~~~~~~~~
-
-This module contains the pure Python database reader and related classes.
-
-"""
-from __future__ import unicode_literals
-
-try:
- import mmap
-except ImportError:
- # pylint: disable=invalid-name
- mmap = None
-
-import struct
-
-from maxminddb.compat import byte_from_int, int_from_byte, compat_ip_address
-from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY
-from maxminddb.decoder import Decoder
-from maxminddb.errors import InvalidDatabaseError
-from maxminddb.file import FileBuffer
-
-
-class Reader(object):
-
- """
- Instances of this class provide a reader for the MaxMind DB format. IP
- addresses can be looked up using the ``get`` method.
- """
-
- _DATA_SECTION_SEPARATOR_SIZE = 16
- _METADATA_START_MARKER = b"\xAB\xCD\xEFMaxMind.com"
-
- _ipv4_start = None
-
- def __init__(self, database, mode=MODE_AUTO):
- """Reader for the MaxMind DB file format
-
- Arguments:
- database -- A path to a valid MaxMind DB file such as a GeoIP2
- database file.
- mode -- mode to open the database with. Valid mode are:
- * MODE_MMAP - read from memory map.
- * MODE_FILE - read database as standard file.
- * MODE_MEMORY - load database into memory.
- * MODE_AUTO - tries MODE_MMAP and then MODE_FILE. Default.
- """
- # pylint: disable=redefined-variable-type
- if (mode == MODE_AUTO and mmap) or mode == MODE_MMAP:
- with open(database, 'rb') as db_file:
- self._buffer = mmap.mmap(
- db_file.fileno(), 0, access=mmap.ACCESS_READ)
- self._buffer_size = self._buffer.size()
- elif mode in (MODE_AUTO, MODE_FILE):
- self._buffer = FileBuffer(database)
- self._buffer_size = self._buffer.size()
- elif mode == MODE_MEMORY:
- with open(database, 'rb') as db_file:
- self._buffer = db_file.read()
- self._buffer_size = len(self._buffer)
- else:
- raise ValueError('Unsupported open mode ({0}). Only MODE_AUTO, '
- ' MODE_FILE, and MODE_MEMORY are support by the pure Python '
- 'Reader'.format(mode))
-
- metadata_start = self._buffer.rfind(self._METADATA_START_MARKER,
- max(0, self._buffer_size
- - 128 * 1024))
-
- if metadata_start == -1:
- self.close()
- raise InvalidDatabaseError('Error opening database file ({0}). '
- 'Is this a valid MaxMind DB file?'
- ''.format(database))
-
- metadata_start += len(self._METADATA_START_MARKER)
- metadata_decoder = Decoder(self._buffer, metadata_start)
- (metadata, _) = metadata_decoder.decode(metadata_start)
- self._metadata = Metadata(
- **metadata) # pylint: disable=bad-option-value
-
- self._decoder = Decoder(self._buffer, self._metadata.search_tree_size
- + self._DATA_SECTION_SEPARATOR_SIZE)
-
- def metadata(self):
- """Return the metadata associated with the MaxMind DB file"""
- return self._metadata
-
- def get(self, ip_address):
- """Return the record for the ip_address in the MaxMind DB
-
-
- Arguments:
- ip_address -- an IP address in the standard string notation
- """
-
- address = compat_ip_address(ip_address)
-
- if address.version == 6 and self._metadata.ip_version == 4:
- raise ValueError('Error looking up {0}. You attempted to look up '
- 'an IPv6 address in an IPv4-only database.'.format(
- ip_address))
- pointer = self._find_address_in_tree(address)
-
- return self._resolve_data_pointer(pointer) if pointer else None
-
- def _find_address_in_tree(self, ip_address):
- packed = ip_address.packed
-
- bit_count = len(packed) * 8
- node = self._start_node(bit_count)
-
- for i in range(bit_count):
- if node >= self._metadata.node_count:
- break
- bit = 1 & (int_from_byte(packed[i >> 3]) >> 7 - (i % 8))
- node = self._read_node(node, bit)
- if node == self._metadata.node_count:
- # Record is empty
- return 0
- elif node > self._metadata.node_count:
- return node
-
- raise InvalidDatabaseError('Invalid node in search tree')
-
- def _start_node(self, length):
- if self._metadata.ip_version != 6 or length == 128:
- return 0
-
- # We are looking up an IPv4 address in an IPv6 tree. Skip over the
- # first 96 nodes.
- if self._ipv4_start:
- return self._ipv4_start
-
- node = 0
- for _ in range(96):
- if node >= self._metadata.node_count:
- break
- node = self._read_node(node, 0)
- self._ipv4_start = node
- return node
-
- def _read_node(self, node_number, index):
- base_offset = node_number * self._metadata.node_byte_size
-
- record_size = self._metadata.record_size
- if record_size == 24:
- offset = base_offset + index * 3
- node_bytes = b'\x00' + self._buffer[offset:offset + 3]
- elif record_size == 28:
- (middle,) = struct.unpack(
- b'!B', self._buffer[base_offset + 3:base_offset + 4])
- if index:
- middle &= 0x0F
- else:
- middle = (0xF0 & middle) >> 4
- offset = base_offset + index * 4
- node_bytes = byte_from_int(
- middle) + self._buffer[offset:offset + 3]
- elif record_size == 32:
- offset = base_offset + index * 4
- node_bytes = self._buffer[offset:offset + 4]
- else:
- raise InvalidDatabaseError(
- 'Unknown record size: {0}'.format(record_size))
- return struct.unpack(b'!I', node_bytes)[0]
-
- def _resolve_data_pointer(self, pointer):
- resolved = pointer - self._metadata.node_count + \
- self._metadata.search_tree_size
-
- if resolved > self._buffer_size:
- raise InvalidDatabaseError(
- "The MaxMind DB file's search tree is corrupt")
-
- (data, _) = self._decoder.decode(resolved)
- return data
-
- def close(self):
- """Closes the MaxMind DB file and returns the resources to the system"""
- # pylint: disable=unidiomatic-typecheck
- if type(self._buffer) not in (str, bytes):
- self._buffer.close()
-
-
-class Metadata(object):
-
- """Metadata for the MaxMind DB reader"""
-
- # pylint: disable=too-many-instance-attributes
- def __init__(self, **kwargs):
- """Creates new Metadata object. kwargs are key/value pairs from spec"""
- # Although I could just update __dict__, that is less obvious and it
- # doesn't work well with static analysis tools and some IDEs
- self.node_count = kwargs['node_count']
- self.record_size = kwargs['record_size']
- self.ip_version = kwargs['ip_version']
- self.database_type = kwargs['database_type']
- self.languages = kwargs['languages']
- self.binary_format_major_version = kwargs[
- 'binary_format_major_version']
- self.binary_format_minor_version = kwargs[
- 'binary_format_minor_version']
- self.build_epoch = kwargs['build_epoch']
- self.description = kwargs['description']
-
- @property
- def node_byte_size(self):
- """The size of a node in bytes"""
- return self.record_size // 4
-
- @property
- def search_tree_size(self):
- """The size of the search tree"""
- return self.node_count * self.node_byte_size
-
- def __repr__(self):
- args = ', '.join('%s=%r' % x for x in self.__dict__.items())
- return '{module}.{class_name}({data})'.format(
- module=self.__module__,
- class_name=self.__class__.__name__,
- data=args)
diff --git a/plexpy/__init__.py b/plexpy/__init__.py
index 81de2a49..2cfa5846 100644
--- a/plexpy/__init__.py
+++ b/plexpy/__init__.py
@@ -472,7 +472,7 @@ def initialize_scheduler():
pms_update_check_hours = CONFIG.PMS_UPDATE_CHECK_INTERVAL if 1 <= CONFIG.PMS_UPDATE_CHECK_INTERVAL else 24
schedule_job(versioncheck.check_update, 'Check GitHub for updates',
- hours=0, minutes=github_minutes, seconds=0, args=(bool(CONFIG.PLEXPY_AUTO_UPDATE), True))
+ hours=0, minutes=github_minutes, seconds=0, args=(True, True))
backup_hours = CONFIG.BACKUP_INTERVAL if 1 <= CONFIG.BACKUP_INTERVAL <= 24 else 6
@@ -480,15 +480,15 @@ def initialize_scheduler():
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
schedule_job(config.make_backup, 'Backup Tautulli config',
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
- schedule_job(helpers.update_geoip_db, 'Update GeoLite2 database',
- hours=12 * bool(CONFIG.GEOIP_DB_INSTALLED), minutes=0, seconds=0)
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0)
+ pms_remote_access_seconds = CONFIG.REMOTE_ACCESS_PING_INTERVAL if 60 <= CONFIG.REMOTE_ACCESS_PING_INTERVAL else 60
+
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
- hours=0, minutes=0, seconds=60 * bool(CONFIG.MONITOR_REMOTE_ACCESS))
+ hours=0, minutes=0, seconds=pms_remote_access_seconds * bool(CONFIG.MONITOR_REMOTE_ACCESS))
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
hours=pms_update_check_hours * bool(CONFIG.MONITOR_PMS_UPDATES), minutes=0, seconds=0)
@@ -612,8 +612,8 @@ def dbcheck():
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, session_id TEXT, '
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
- 'ip_address TEXT, machine_id TEXT, player TEXT, product TEXT, platform TEXT, title TEXT, parent_title TEXT, '
- 'grandparent_title TEXT, original_title TEXT, full_title TEXT, '
+ 'ip_address TEXT, machine_id TEXT, bandwidth INTEGER, location TEXT, player TEXT, product TEXT, platform TEXT, '
+ 'title TEXT, parent_title TEXT, grandparent_title TEXT, original_title TEXT, full_title TEXT, '
'media_index INTEGER, parent_media_index INTEGER, '
'thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, year INTEGER, '
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
@@ -640,7 +640,13 @@ def dbcheck():
'live INTEGER, live_uuid TEXT, channel_call_sign TEXT, channel_identifier TEXT, channel_thumb TEXT, '
'secure INTEGER, relayed INTEGER, '
'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, watched INTEGER DEFAULT 0, '
- 'write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)'
+ 'initial_stream INTEGER DEFAULT 1, write_attempts INTEGER DEFAULT 0, raw_stream_info TEXT)'
+ )
+
+ # sessions_continued table :: This is a temp table that keeps track of continued streaming sessions
+ c_db.execute(
+ 'CREATE TABLE IF NOT EXISTS sessions_continued (id INTEGER PRIMARY KEY AUTOINCREMENT, '
+ 'user_id INTEGER, machine_id TEXT, media_type TEXT, stopped INTEGER)'
)
# session_history table :: This is a history table which logs essential stream details
@@ -1294,6 +1300,27 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN guid TEXT'
)
+ # Upgrade sessions table from earlier versions
+ try:
+ c_db.execute('SELECT bandwidth FROM sessions')
+ except sqlite3.OperationalError:
+ logger.debug(u"Altering database. Updating database table sessions.")
+ c_db.execute(
+ 'ALTER TABLE sessions ADD COLUMN bandwidth INTEGER'
+ )
+ c_db.execute(
+ 'ALTER TABLE sessions ADD COLUMN location TEXT'
+ )
+
+ # Upgrade sessions table from earlier versions
+ try:
+ c_db.execute('SELECT initial_stream FROM sessions')
+ except sqlite3.OperationalError:
+ logger.debug(u"Altering database. Updating database table sessions.")
+ c_db.execute(
+ 'ALTER TABLE sessions ADD COLUMN initial_stream INTEGER DEFAULT 1'
+ )
+
# Upgrade session_history table from earlier versions
try:
c_db.execute('SELECT reference_id FROM session_history')
diff --git a/plexpy/activity_handler.py b/plexpy/activity_handler.py
index bb3000e5..91d8d382 100644
--- a/plexpy/activity_handler.py
+++ b/plexpy/activity_handler.py
@@ -96,14 +96,14 @@ class ActivityHandler(object):
return None
- def update_db_session(self, session=None):
+ def update_db_session(self, session=None, notify=False):
if session is None:
session = self.get_live_session()
if session:
# Update our session temp table values
ap = activity_processor.ActivityProcessor()
- ap.write_session(session=session, notify=False)
+ ap.write_session(session=session, notify=notify)
self.set_session_state()
@@ -133,10 +133,11 @@ class ActivityHandler(object):
% (str(session['session_key']), str(session['user_id']), session['username'],
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
- plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
+ # Send notification after updating db
+ #plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
# Write the new session to our temp session table
- self.update_db_session(session=session)
+ self.update_db_session(session=session, notify=True)
# Schedule a callback to force stop a stale stream 5 minutes later
schedule_callback('session_key-{}'.format(self.get_session_key()),
diff --git a/plexpy/activity_pinger.py b/plexpy/activity_pinger.py
index d284dac8..94927cf0 100644
--- a/plexpy/activity_pinger.py
+++ b/plexpy/activity_pinger.py
@@ -17,7 +17,6 @@ from __future__ import unicode_literals
from future.builtins import str
import threading
-import time
import plexpy
if plexpy.PYTHON2:
@@ -327,31 +326,27 @@ def check_server_access():
# Check for remote access
if server_response:
-
- mapping_state = server_response['mapping_state']
- mapping_error = server_response['mapping_error']
-
- # Check if the port is mapped
- if not mapping_state == 'mapped':
+ if server_response['reason']:
ext_ping_count += 1
- logger.warn("Tautulli Monitor :: Plex remote access port not mapped, ping attempt %s." \
- % str(ext_ping_count))
- # Check if the port is open
- elif mapping_error == 'unreachable':
- ext_ping_count += 1
- logger.warn("Tautulli Monitor :: Plex remote access port mapped, but mapping failed, ping attempt %s." \
+ logger.warn("Tautulli Monitor :: Remote access failed: %s, ping attempt %s." \
+ % (server_response['reason'], str(ext_ping_count)))
+
+ # Waiting for port mapping
+ elif server_response['mapping_state'] == 'waiting':
+ logger.warn("Tautulli Monitor :: Remote access waiting for port mapping, ping attempt %s." \
% str(ext_ping_count))
+
# Reset external ping counter
else:
if ext_ping_count >= plexpy.CONFIG.REMOTE_ACCESS_PING_THRESHOLD:
logger.info("Tautulli Monitor :: Plex remote access is back up.")
- plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extup'})
+ plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
ext_ping_count = 0
if ext_ping_count == plexpy.CONFIG.REMOTE_ACCESS_PING_THRESHOLD:
- plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown'})
+ plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
def check_server_updates():
diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py
index 51633948..53cebc0e 100644
--- a/plexpy/activity_processor.py
+++ b/plexpy/activity_processor.py
@@ -19,7 +19,6 @@ from future.builtins import object
from collections import defaultdict
import json
-import time
import plexpy
if plexpy.PYTHON2:
@@ -68,6 +67,8 @@ class ActivityProcessor(object):
'year': session.get('year', ''),
'friendly_name': session.get('friendly_name', ''),
'ip_address': session.get('ip_address', ''),
+ 'bandwidth': session.get('bandwidth', 0),
+ 'location': session.get('location', ''),
'player': session.get('player', ''),
'product': session.get('product', ''),
'platform': session.get('platform', ''),
@@ -152,15 +153,20 @@ class ActivityProcessor(object):
result = self.db.upsert('sessions', values, keys)
if result == 'insert':
- # Check if any notification agents have notifications enabled
- if notify:
- plexpy.NOTIFY_QUEUE.put({'stream_data': values.copy(), 'notify_action': 'on_play'})
-
# If it's our first write then time stamp it.
started = helpers.timestamp()
- timestamp = {'started': started}
+ initial_stream = self.is_initial_stream(user_id=values['user_id'],
+ machine_id=values['machine_id'],
+ media_type=values['media_type'],
+ started=started)
+ timestamp = {'started': started, 'initial_stream': initial_stream}
self.db.upsert('sessions', timestamp, keys)
+ # Check if any notification agents have notifications enabled
+ if notify:
+ session.update(timestamp)
+ plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
+
# Add Live TV library if it hasn't been added
if values['live']:
libraries.add_live_tv_library()
@@ -209,6 +215,12 @@ class ActivityProcessor(object):
state='stopped',
stopped=stopped)
+ if not is_import:
+ self.write_continued_session(user_id=session['user_id'],
+ machine_id=session['machine_id'],
+ media_type=session['media_type'],
+ stopped=stopped)
+
if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'):
logging_enabled = True
else:
@@ -637,3 +649,16 @@ class ActivityProcessor(object):
self.db.action('UPDATE sessions SET watched = ?'
'WHERE session_key = ?',
[1, session_key])
+
+ def write_continued_session(self, user_id=None, machine_id=None, media_type=None, stopped=None):
+ keys = {'user_id': user_id, 'machine_id': machine_id, 'media_type': media_type}
+ values = {'stopped': stopped}
+ self.db.upsert(table_name='sessions_continued', key_dict=keys, value_dict=values)
+
+ def is_initial_stream(self, user_id=None, machine_id=None, media_type=None, started=None):
+ last_session = self.db.select_single('SELECT stopped '
+ 'FROM sessions_continued '
+ 'WHERE user_id = ? AND machine_id = ? AND media_type = ? '
+ 'ORDER BY stopped DESC',
+ [user_id, machine_id, media_type])
+ return int(started - last_session.get('stopped', 0) >= plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)
diff --git a/plexpy/api2.py b/plexpy/api2.py
index 5fa203e4..0d9e61c8 100644
--- a/plexpy/api2.py
+++ b/plexpy/api2.py
@@ -631,6 +631,12 @@ General optional parameters:
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
return out['response']['data']
+ elif self._api_cmd == 'get_geoip_lookup':
+ # Remove nested data and put error message inside data for backwards compatibility
+ out['response']['data'] = out['response']['data'].get('data')
+ if not out['response']['data']:
+ out['response']['data'] = {'error': out['response']['message']}
+
if self._api_out_type == 'json':
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
try:
diff --git a/plexpy/common.py b/plexpy/common.py
index ab1a0501..fcb1372a 100644
--- a/plexpy/common.py
+++ b/plexpy/common.py
@@ -224,8 +224,7 @@ SCHEDULER_LIST = [
'Refresh libraries list',
'Refresh Plex server URLs',
'Backup Tautulli database',
- 'Backup Tautulli config',
- 'Update GeoLite2 database'
+ 'Backup Tautulli config'
]
DATE_TIME_FORMATS = [
@@ -350,10 +349,13 @@ NOTIFICATION_PARAMETERS = [
{
'category': 'Stream Details',
'parameters': [
- {'name': 'Streams', 'type': 'int', 'value': 'streams', 'description': 'The number of concurrent streams.'},
- {'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays', 'description': 'The number of concurrent direct plays.'},
- {'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams', 'description': 'The number of concurrent direct streams.'},
- {'name': 'Transcodes', 'type': 'int', 'value': 'transcodes', 'description': 'The number of concurrent transcodes.'},
+ {'name': 'Streams', 'type': 'int', 'value': 'streams', 'description': 'The total number of concurrent streams.'},
+ {'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays', 'description': 'The total number of concurrent direct plays.'},
+ {'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams', 'description': 'The total number of concurrent direct streams.'},
+ {'name': 'Transcodes', 'type': 'int', 'value': 'transcodes', 'description': 'The total number of concurrent transcodes.'},
+ {'name': 'Total Bandwidth', 'type': 'int', 'value': 'total_bandwidth', 'description': 'The total Plex Streaming Brain reserved bandwidth (in kbps).', 'help_text': 'not the used bandwidth'},
+ {'name': 'LAN Bandwidth', 'type': 'int', 'value': 'lan_bandwidth', 'description': 'The total Plex Streaming Brain reserved LAN bandwidth (in kbps).', 'help_text': 'not the used bandwidth'},
+ {'name': 'WAN Bandwidth', 'type': 'int', 'value': 'wan_bandwidth', 'description': 'The total Plex Streaming Brain reserved WAN bandwidth (in kbps).', 'help_text': 'not the used bandwidth'},
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the user streaming.'},
{'name': 'User Direct Plays', 'type': 'int', 'value': 'user_direct_plays', 'description': 'The number of concurrent direct plays by the user streaming.'},
{'name': 'User Direct Streams', 'type': 'int', 'value': 'user_direct_streams', 'description': 'The number of concurrent direct streams by the user streaming.'},
@@ -361,10 +363,12 @@ NOTIFICATION_PARAMETERS = [
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the user streaming.'},
{'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the user streaming.'},
{'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the user streaming.'},
+ {'name': 'User Thumb', 'type': 'str', 'value': 'user_thumb', 'description': 'The profile picture URL of the user streaming.'},
{'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'},
{'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'},
{'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'},
{'name': 'Player', 'type': 'str', 'value': 'player', 'description': 'The name of the player being used for playback.'},
+ {'name': 'Initial Stream', 'type': 'int', 'value': 'initial_stream', 'description': 'If the stream is the initial stream of a continuous streaming session.', 'example': '0 or 1'},
{'name': 'IP Address', 'type': 'str', 'value': 'ip_address', 'description': 'The IP address of the device being used for playback.'},
{'name': 'Stream Duration', 'type': 'int', 'value': 'stream_duration', 'description': 'The duration (in minutes) for the stream.'},
{'name': 'Stream Time', 'type': 'str', 'value': 'stream_time', 'description': 'The duration (in time format) of the stream.'},
@@ -389,7 +393,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.', 'example': '0 or 1'},
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'},
{'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', 'description': 'The network location of the stream.', 'example': 'lan or wan'},
- {'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The required bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'},
+ {'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The Plex Streaming Brain reserved bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'},
{'name': 'Stream Container', 'type': 'str', 'value': 'stream_container', 'description': 'The media container of the stream.'},
{'name': 'Stream Bitrate', 'type': 'int', 'value': 'stream_bitrate', 'description': 'The bitrate (in kbps) of the stream.'},
{'name': 'Stream Aspect Ratio', 'type': 'float', 'value': 'stream_aspect_ratio', 'description': 'The aspect ratio of the stream.'},
@@ -556,6 +560,18 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Indexes', 'type': 'int', 'value': 'indexes', 'description': 'If the media has video preview thumbnails.', 'example': '0 or 1'},
]
},
+ {
+ 'category': 'Plex Remote Access',
+ 'parameters': [
+ {'name': 'Remote Access Mapping State', 'type': 'str', 'value': 'remote_access_mapping_state', 'description': 'The mapping state of the Plex remote access port.'},
+ {'name': 'Remote Access Mapping Error', 'type': 'str', 'value': 'remote_access_mapping_error', 'description': 'The mapping error of the Plex remote access port.'},
+ {'name': 'Remote Access Public IP Address', 'type': 'str', 'value': 'remote_access_public_address', 'description': 'The Plex remote access public IP address.'},
+ {'name': 'Remote Access Public Port', 'type': 'str', 'value': 'remote_access_public_port', 'description': 'The Plex remote access public port.'},
+ {'name': 'Remote Access Private IP Address', 'type': 'str', 'value': 'remote_access_private_address', 'description': 'The Plex remote access private IP address.'},
+ {'name': 'Remote Access Private Port', 'type': 'str', 'value': 'remote_access_private_port', 'description': 'The Plex remote access private port.'},
+ {'name': 'Remote Access Failure Reason', 'type': 'str', 'value': 'remote_access_reason', 'description': 'The failure reason for Plex remote access going down.'},
+ ]
+ },
{
'category': 'Plex Update Available',
'parameters': [
diff --git a/plexpy/config.py b/plexpy/config.py
index 7403cdb3..1856cad3 100644
--- a/plexpy/config.py
+++ b/plexpy/config.py
@@ -182,9 +182,6 @@ _CONFIG_DEFINITIONS = {
'FACEBOOK_ON_NEWDEVICE': (int, 'Facebook', 0),
'FIRST_RUN_COMPLETE': (int, 'General', 0),
'FREEZE_DB': (int, 'General', 0),
- 'GEOIP_DB': (str, 'General', ''),
- 'GEOIP_DB_INSTALLED': (int, 'General', 0),
- 'GEOIP_DB_UPDATE_DAYS': (int, 'General', 30),
'GET_FILE_SIZES': (int, 'General', 0),
'GET_FILE_SIZES_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
'GIT_BRANCH': (str, 'General', 'master'),
@@ -299,7 +296,6 @@ _CONFIG_DEFINITIONS = {
'LOG_BLACKLIST': (int, 'General', 1),
'LOG_DIR': (str, 'General', ''),
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
- 'MAXMIND_LICENSE_KEY': (str, 'General', ''),
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
@@ -345,6 +341,7 @@ _CONFIG_DEFINITIONS = {
'NMA_ON_NEWDEVICE': (int, 'NMA', 0),
'NOTIFICATION_THREADS': (int, 'Advanced', 2),
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
+ 'NOTIFY_CONTINUED_SESSION_THRESHOLD': (int, 'Monitoring', 15),
'NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 1),
'NOTIFY_GROUP_RECENTLY_ADDED_PARENT': (int, 'Monitoring', 1),
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
@@ -497,6 +494,7 @@ _CONFIG_DEFINITIONS = {
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
+ 'REMOTE_ACCESS_PING_INTERVAL': (int, 'Advanced', 60),
'REMOTE_ACCESS_PING_THRESHOLD': (int, 'Advanced', 3),
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
'SHOW_ADVANCED_SETTINGS': (int, 'General', 0),
@@ -937,8 +935,6 @@ class Config(object):
self.CONFIG_VERSION = 13
if self.CONFIG_VERSION == 13:
- if not self.GEOIP_DB:
- self.GEOIP_DB = os.path.join(plexpy.DATA_DIR, 'GeoLite2-City.mmdb')
self.CONFIG_VERSION = 14
diff --git a/plexpy/database.py b/plexpy/database.py
index 0d6637a2..91b5f3f7 100644
--- a/plexpy/database.py
+++ b/plexpy/database.py
@@ -250,7 +250,7 @@ class MonitorDatabase(object):
sql_results = self.action(query, args).fetchone()
if sql_results is None or sql_results == "":
- return ""
+ return {}
return sql_results
diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py
index c546352e..544b3e20 100644
--- a/plexpy/datafactory.py
+++ b/plexpy/datafactory.py
@@ -246,6 +246,7 @@ class DataFactory(object):
row = {'reference_id': item['reference_id'],
'row_id': item['row_id'],
+ 'id': item['row_id'],
'date': item['date'],
'started': item['started'],
'stopped': item['stopped'],
diff --git a/plexpy/helpers.py b/plexpy/helpers.py
index 17e88c75..d3b4b25f 100644
--- a/plexpy/helpers.py
+++ b/plexpy/helpers.py
@@ -23,15 +23,12 @@ from future.builtins import str
import arrow
import base64
-import certifi
import cloudinary
from cloudinary.api import delete_resources_by_tag
from cloudinary.uploader import upload
from cloudinary.utils import cloudinary_url
import datetime
from functools import wraps
-import geoip2.database
-import geoip2.errors
import hashlib
import imghdr
from future.moves.itertools import zip_longest
@@ -41,19 +38,14 @@ import ipwhois.utils
from IPy import IP
import json
import math
-import maxminddb
-from operator import itemgetter
import os
import re
import shlex
-import shutil
import socket
import sys
-import tarfile
import time
import unicodedata
from future.moves.urllib.parse import urlencode
-import urllib3
from xml.dom import minidom
import xmltodict
@@ -612,164 +604,6 @@ def is_valid_ip(address):
return False
-def update_geoip_db():
- if plexpy.CONFIG.GEOIP_DB_INSTALLED:
- logger.info("Tautulli Helpers :: Checking for GeoLite2 database updates.")
- now = timestamp()
- if now - plexpy.CONFIG.GEOIP_DB_INSTALLED >= plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS * 24 * 60 * 60:
- return install_geoip_db(update=True)
- logger.info("Tautulli Helpers :: GeoLite2 database already updated within the last %s days."
- % plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS)
-
-
-def install_geoip_db(update=False):
- if not plexpy.CONFIG.MAXMIND_LICENSE_KEY:
- logger.error("Tautulli Helpers :: Failed to download GeoLite2 database file from MaxMind: Missing MaxMindLicense Key")
- return False
-
- maxmind_db = 'GeoLite2-City'
- maxmind_url = 'https://download.maxmind.com/app/geoip_download?edition_id={db}&suffix={{suffix}}&license_key={key}'.format(
- db=maxmind_db, key=plexpy.CONFIG.MAXMIND_LICENSE_KEY)
- geolite2_db_url = maxmind_url.format(suffix='tar.gz')
- geolite2_md5_url = maxmind_url.format(suffix='tar.gz.md5')
- geolite2_gz = maxmind_db + '.tar.gz'
- geolite2_md5 = geolite2_gz + '.md5'
- geolite2_db = maxmind_db + '.mmdb'
- geolite2_db_path = plexpy.CONFIG.GEOIP_DB or os.path.join(plexpy.DATA_DIR, geolite2_db)
-
- # Check path ends with .mmdb
- if os.path.splitext(geolite2_db_path)[1] != os.path.splitext(geolite2_db)[1]:
- geolite2_db_path = os.path.join(geolite2_db_path, geolite2_db)
-
- temp_gz = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_gz)
- temp_md5 = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_md5)
-
- # Retrieve the GeoLite2 gzip file
- logger.debug("Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
- try:
- maxmind = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
- with maxmind.request('GET', geolite2_db_url, preload_content=False) as r_db, open(temp_gz, 'wb') as f_db:
- shutil.copyfileobj(r_db, f_db)
- with maxmind.request('GET', geolite2_md5_url, preload_content=False) as r_md5, open(temp_md5, 'wb') as f_md5:
- shutil.copyfileobj(r_md5, f_md5)
- except Exception as e:
- logger.error("Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
- return False
-
- # Check MD5 hash for GeoLite2 tar.gz file
- logger.debug("Tautulli Helpers :: Checking MD5 checksum for GeoLite2 gzip file...")
- try:
- hash_md5 = hashlib.md5()
- with open(temp_gz, 'rb') as f:
- for chunk in iter(lambda: f.read(4096), b""):
- hash_md5.update(chunk)
- md5_hash = hash_md5.hexdigest()
-
- with open(temp_md5, 'r') as f:
- md5_checksum = f.read()
-
- if md5_hash != md5_checksum:
- logger.error("Tautulli Helpers :: MD5 checksum doesn't match for GeoLite2 database. "
- "Checksum: %s, file hash: %s" % (md5_checksum, md5_hash))
- return False
- except Exception as e:
- logger.error("Tautulli Helpers :: Failed to generate MD5 checksum for GeoLite2 gzip file: %s" % e)
- return False
-
- # Extract the GeoLite2 database file
- logger.debug("Tautulli Helpers :: Extracting GeoLite2 database...")
- try:
- mmdb = None
- with tarfile.open(temp_gz, 'r:gz') as tar:
- for member in tar.getmembers():
- if geolite2_db in member.name:
- member.name = os.path.basename(member.name)
- tar.extractall(path=os.path.dirname(geolite2_db_path), members=[member])
- mmdb = True
- break
- if not mmdb:
- raise Exception("{} not found in gzip file.".format(geolite2_db))
- except Exception as e:
- logger.error("Tautulli Helpers :: Failed to extract the GeoLite2 database: %s" % e)
- return False
-
- # Delete temportary GeoLite2 gzip file
- logger.debug("Tautulli Helpers :: Deleting temporary GeoLite2 gzip file...")
- try:
- os.remove(temp_gz)
- os.remove(temp_md5)
- except Exception as e:
- logger.warn("Tautulli Helpers :: Failed to remove temporary GeoLite2 gzip file: %s" % e)
-
- plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db_path)
- plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', timestamp())
- plexpy.CONFIG.write()
-
- logger.debug("Tautulli Helpers :: GeoLite2 database installed successfully.")
-
- if not update:
- plexpy.schedule_job(update_geoip_db, 'Update GeoLite2 database', hours=12, minutes=0, seconds=0)
-
- return plexpy.CONFIG.GEOIP_DB_INSTALLED
-
-
-def uninstall_geoip_db():
- logger.debug("Tautulli Helpers :: Uninstalling the GeoLite2 database...")
- try:
- os.remove(plexpy.CONFIG.GEOIP_DB)
- except Exception as e:
- logger.error("Tautulli Helpers :: Failed to uninstall the GeoLite2 database: %s" % e)
- return False
-
- plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', 0)
- plexpy.CONFIG.write()
-
- logger.debug("Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
-
- plexpy.schedule_job(update_geoip_db, 'Update GeoLite2 database', hours=0, minutes=0, seconds=0)
-
- return True
-
-
-def geoip_lookup(ip_address):
- if not plexpy.CONFIG.GEOIP_DB_INSTALLED:
- return 'GeoLite2 database not installed. Please install from the ' \
- 'Settings page.'
-
- if not ip_address:
- return 'No IP address provided.'
-
- try:
- reader = geoip2.database.Reader(plexpy.CONFIG.GEOIP_DB)
- geo = reader.city(ip_address)
- reader.close()
- except ValueError as e:
- return 'Invalid IP address provided: %s.' % ip_address
- except IOError as e:
- return 'Missing GeoLite2 database. Please reinstall from the ' \
- 'Settings page.'
- except maxminddb.InvalidDatabaseError as e:
- return 'Invalid GeoLite2 database. Please reinstall from the ' \
- 'Settings page.'
- except geoip2.errors.AddressNotFoundError as e:
- return '%s' % e
- except Exception as e:
- return 'Error: %s' % e
-
- geo_info = {'continent': geo.continent.name,
- 'country': geo.country.name,
- 'region': geo.subdivisions.most_specific.name,
- 'city': geo.city.name,
- 'postal_code': geo.postal.code,
- 'timezone': geo.location.time_zone,
- 'latitude': geo.location.latitude,
- 'longitude': geo.location.longitude,
- 'accuracy': geo.location.accuracy_radius
- }
-
- return geo_info
-
-
def whois_lookup(ip_address):
nets = []
diff --git a/plexpy/libraries.py b/plexpy/libraries.py
index 16f33bda..4a828239 100644
--- a/plexpy/libraries.py
+++ b/plexpy/libraries.py
@@ -755,7 +755,7 @@ class Libraries(object):
except Exception as e:
logger.warn("Tautulli Libraries :: Unable to execute database query for set_config: %s." % e)
- def get_details(self, section_id=None):
+ def get_details(self, section_id=None, server_id=None):
default_return = {'row_id': 0,
'server_id': '',
'section_id': 0,
@@ -776,7 +776,10 @@ class Libraries(object):
if not section_id:
return default_return
- def get_library_details(section_id=section_id):
+ if server_id is None:
+ server_id = plexpy.CONFIG.PMS_IDENTIFIER
+
+ def get_library_details(section_id=section_id, server_id=server_id):
monitor_db = database.MonitorDatabase()
try:
@@ -787,8 +790,8 @@ class Libraries(object):
'custom_art_url AS custom_art, is_active, ' \
'do_notify, do_notify_created, keep_history, deleted_section ' \
'FROM library_sections ' \
- 'WHERE section_id = ? '
- result = monitor_db.select(query, args=[section_id])
+ 'WHERE section_id = ? AND server_id = ? '
+ result = monitor_db.select(query, args=[section_id, server_id])
else:
result = []
except Exception as e:
@@ -828,7 +831,7 @@ class Libraries(object):
}
return library_details
- library_details = get_library_details(section_id=section_id)
+ library_details = get_library_details(section_id=section_id, server_id=server_id)
if library_details:
return library_details
@@ -839,7 +842,7 @@ class Libraries(object):
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
refresh_libraries()
- library_details = get_library_details(section_id=section_id)
+ library_details = get_library_details(section_id=section_id, server_id=server_id)
if library_details:
return library_details
diff --git a/plexpy/newsletter_handler.py b/plexpy/newsletter_handler.py
index 5882579d..7a4492c7 100644
--- a/plexpy/newsletter_handler.py
+++ b/plexpy/newsletter_handler.py
@@ -18,7 +18,6 @@
from __future__ import unicode_literals
import os
-import time
from apscheduler.triggers.cron import CronTrigger
import email.utils
diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py
index f6f467c3..a4779dab 100644
--- a/plexpy/notification_handler.py
+++ b/plexpy/notification_handler.py
@@ -565,6 +565,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
stream_count = len(sessions)
user_stream_count = len(user_sessions)
+ lan_bandwidth = sum(helpers.cast_to_int(s['bandwidth']) for s in sessions if s['location'] == 'lan')
+ wan_bandwidth = sum(helpers.cast_to_int(s['bandwidth']) for s in sessions if s['location'] != 'lan')
+ total_bandwidth = lan_bandwidth + wan_bandwidth
+
# Generate a combined transcode decision value
if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode':
transcode_decision = 'Transcode'
@@ -650,6 +654,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
themoviedb_info = lookup_themoviedb_by_id(rating_key=lookup_key,
thetvdb_id=notify_params.get('thetvdb_id'),
imdb_id=notify_params.get('imdb_id'))
+ themoviedb_info.pop('rating_key', None)
notify_params.update(themoviedb_info)
# Get TVmaze info (for tv shows only)
@@ -665,6 +670,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
tvmaze_info = lookup_tvmaze_by_id(rating_key=lookup_key,
thetvdb_id=notify_params.get('thetvdb_id'),
imdb_id=notify_params.get('imdb_id'))
+ tvmaze_info.pop('rating_key', None)
notify_params.update(tvmaze_info)
if tvmaze_info.get('thetvdb_id'):
@@ -685,7 +691,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
tracks = notify_params['children_count']
else:
musicbrainz_type = 'recording'
- artist = notify_params['original_title']
+ artist = notify_params['original_title'] or notify_params['grandparent_title']
release = notify_params['parent_title']
recording = notify_params['title']
tracks = notify_params['children_count']
@@ -694,6 +700,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
musicbrainz_info = lookup_musicbrainz_info(musicbrainz_type=musicbrainz_type, rating_key=rating_key,
artist=artist, release=release, recording=recording, tracks=tracks,
tnum=tnum)
+ musicbrainz_info.pop('rating_key', None)
notify_params.update(musicbrainz_info)
if notify_params['media_type'] in ('movie', 'show', 'artist'):
@@ -831,6 +838,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'direct_plays': transcode_decision_count['direct play'],
'direct_streams': transcode_decision_count['copy'],
'transcodes': transcode_decision_count['transcode'],
+ 'total_bandwidth': total_bandwidth,
+ 'lan_bandwidth': lan_bandwidth,
+ 'wan_bandwidth': wan_bandwidth,
'user_streams': user_stream_count,
'user_direct_plays': user_transcode_decision_count['direct play'],
'user_direct_streams': user_transcode_decision_count['copy'],
@@ -838,6 +848,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'user': notify_params['friendly_name'],
'username': notify_params['user'],
'user_email': notify_params['email'],
+ 'user_thumb': notify_params['user_thumb'],
'device': notify_params['device'],
'platform': notify_params['platform'],
'product': notify_params['product'],
@@ -850,6 +861,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'progress_duration': view_offset,
'progress_time': arrow.get(view_offset * 60).format(duration_format),
'progress_percent': helpers.get_percent(view_offset, duration),
+ 'initial_stream': notify_params['initial_stream'],
'transcode_decision': transcode_decision,
'video_decision': notify_params['video_decision'],
'audio_decision': notify_params['audio_decision'],
@@ -1047,6 +1059,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
+ remote_access_info = defaultdict(str, kwargs.pop('remote_access_info', {}))
now = arrow.now()
now_iso = now.isocalendar()
@@ -1078,6 +1091,14 @@ def build_server_notify_params(notify_action=None, **kwargs):
'timestamp': now.format(time_format),
'unixtime': helpers.timestamp(),
'utctime': helpers.utc_now_iso(),
+ # Plex remote access parameters
+ 'remote_access_mapping_state': remote_access_info['mapping_state'],
+ 'remote_access_mapping_error': remote_access_info['mapping_error'],
+ 'remote_access_public_address': remote_access_info['public_address'],
+ 'remote_access_public_port': remote_access_info['public_port'],
+ 'remote_access_private_address': remote_access_info['private_address'],
+ 'remote_access_private_port': remote_access_info['private_port'],
+ 'remote_access_reason': remote_access_info['reason'],
# Plex Media Server update parameters
'update_version': pms_download_info['version'],
'update_url': pms_download_info['download_url'],
diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py
index 20238a72..b9bcec3f 100644
--- a/plexpy/notifiers.py
+++ b/plexpy/notifiers.py
@@ -16,7 +16,6 @@
# along with Tautulli. If not, see .
from __future__ import unicode_literals
-from future.builtins import next
from future.builtins import str
from future.builtins import object
@@ -81,7 +80,6 @@ else:
BROWSER_NOTIFIERS = {}
-
AGENT_IDS = {'growl': 0,
'prowl': 1,
'xbmc': 2,
@@ -104,7 +102,8 @@ AGENT_IDS = {'growl': 0,
'groupme': 22,
'mqtt': 23,
'zapier': 24,
- 'webhook': 25
+ 'webhook': 25,
+ 'plexmobileapp': 26
}
DEFAULT_CUSTOM_CONDITIONS = [{'parameter': '', 'operator': '', 'value': ''}]
@@ -113,91 +112,141 @@ DEFAULT_CUSTOM_CONDITIONS = [{'parameter': '', 'operator': '', 'value': ''}]
def available_notification_agents():
agents = [{'label': 'Tautulli Remote Android App',
'name': 'androidapp',
- 'id': AGENT_IDS['androidapp']
+ 'id': AGENT_IDS['androidapp'],
+ 'class': ANDROIDAPP,
+ 'action_types': ('all',)
},
{'label': 'Boxcar',
'name': 'boxcar',
- 'id': AGENT_IDS['boxcar']
+ 'id': AGENT_IDS['boxcar'],
+ 'class': BOXCAR,
+ 'action_types': ('all',)
},
{'label': 'Browser',
'name': 'browser',
- 'id': AGENT_IDS['browser']
+ 'id': AGENT_IDS['browser'],
+ 'class': BROWSER,
+ 'action_types': ('all',)
},
{'label': 'Discord',
'name': 'discord',
'id': AGENT_IDS['discord'],
+ 'class': DISCORD,
+ 'action_types': ('all',)
},
{'label': 'Email',
'name': 'email',
- 'id': AGENT_IDS['email']
+ 'id': AGENT_IDS['email'],
+ 'class': EMAIL,
+ 'action_types': ('all',)
},
{'label': 'Facebook',
'name': 'facebook',
- 'id': AGENT_IDS['facebook']
+ 'id': AGENT_IDS['facebook'],
+ 'class': FACEBOOK,
+ 'action_types': ('all',)
},
{'label': 'GroupMe',
'name': 'groupme',
- 'id': AGENT_IDS['groupme']
+ 'id': AGENT_IDS['groupme'],
+ 'class': GROUPME,
+ 'action_types': ('all',)
},
{'label': 'Growl',
'name': 'growl',
- 'id': AGENT_IDS['growl']
+ 'id': AGENT_IDS['growl'],
+ 'class': GROWL,
+ 'action_types': ('all',)
},
{'label': 'IFTTT',
'name': 'ifttt',
- 'id': AGENT_IDS['ifttt']
+ 'id': AGENT_IDS['ifttt'],
+ 'class': IFTTT,
+ 'action_types': ('all',)
},
{'label': 'Join',
'name': 'join',
- 'id': AGENT_IDS['join']
+ 'id': AGENT_IDS['join'],
+ 'class': JOIN,
+ 'action_types': ('all',)
},
{'label': 'Kodi',
'name': 'xbmc',
- 'id': AGENT_IDS['xbmc']
+ 'id': AGENT_IDS['xbmc'],
+ 'class': XBMC,
+ 'action_types': ('all',)
},
{'label': 'MQTT',
'name': 'mqtt',
- 'id': AGENT_IDS['mqtt']
+ 'id': AGENT_IDS['mqtt'],
+ 'class': MQTT,
+ 'action_types': ('all',)
},
{'label': 'Plex Home Theater',
'name': 'plex',
- 'id': AGENT_IDS['plex']
+ 'id': AGENT_IDS['plex'],
+ 'class': PLEX,
+ 'action_types': ('all',)
+ },
+ {'label': 'Plex Android / iOS App',
+ 'name': 'plexmobileapp',
+ 'id': AGENT_IDS['plexmobileapp'],
+ 'class': PLEXMOBILEAPP,
+ 'action_types': ('on_play', 'on_created', 'on_newdevice')
},
{'label': 'Prowl',
'name': 'prowl',
- 'id': AGENT_IDS['prowl']
+ 'id': AGENT_IDS['prowl'],
+ 'class': PROWL,
+ 'action_types': ('all',)
},
{'label': 'Pushbullet',
'name': 'pushbullet',
- 'id': AGENT_IDS['pushbullet']
+ 'id': AGENT_IDS['pushbullet'],
+ 'class': PUSHBULLET,
+ 'action_types': ('all',)
},
{'label': 'Pushover',
'name': 'pushover',
- 'id': AGENT_IDS['pushover']
+ 'id': AGENT_IDS['pushover'],
+ 'class': PUSHOVER,
+ 'action_types': ('all',)
},
{'label': 'Script',
'name': 'scripts',
- 'id': AGENT_IDS['scripts']
+ 'id': AGENT_IDS['scripts'],
+ 'class': SCRIPTS,
+ 'action_types': ('all',)
},
{'label': 'Slack',
'name': 'slack',
- 'id': AGENT_IDS['slack']
+ 'id': AGENT_IDS['slack'],
+ 'class': SLACK,
+ 'action_types': ('all',)
},
{'label': 'Telegram',
'name': 'telegram',
- 'id': AGENT_IDS['telegram']
+ 'id': AGENT_IDS['telegram'],
+ 'class': TELEGRAM,
+ 'action_types': ('all',)
},
{'label': 'Twitter',
'name': 'twitter',
- 'id': AGENT_IDS['twitter']
+ 'id': AGENT_IDS['twitter'],
+ 'class': TWITTER,
+ 'action_types': ('all',)
},
{'label': 'Webhook',
'name': 'webhook',
- 'id': AGENT_IDS['webhook']
+ 'id': AGENT_IDS['webhook'],
+ 'class': WEBHOOK,
+ 'action_types': ('all',)
},
{'label': 'Zapier',
'name': 'zapier',
- 'id': AGENT_IDS['zapier']
+ 'id': AGENT_IDS['zapier'],
+ 'class': ZAPIER,
+ 'action_types': ('all',)
}
]
@@ -205,13 +254,15 @@ def available_notification_agents():
if OSX().validate():
agents.append({'label': 'macOS Notification Center',
'name': 'osx',
- 'id': AGENT_IDS['osx']
+ 'id': AGENT_IDS['osx'],
+ 'class': OSX,
+ 'action_types': ('all',)
})
return agents
-def available_notification_actions():
+def available_notification_actions(agent_id=None):
actions = [{'label': 'Playback Start',
'name': 'on_play',
'description': 'Trigger a notification when a stream is started.',
@@ -312,7 +363,7 @@ def available_notification_actions():
'name': 'on_extdown',
'description': 'Trigger a notification when the Plex Media Server cannot be reached externally.',
'subject': 'Tautulli ({server_name})',
- 'body': 'The Plex Media Server remote access is down.',
+ 'body': 'The Plex Media Server remote access is down. ({remote_access_reason})',
'icon': 'fa-server',
'media_types': ('server',)
},
@@ -350,72 +401,31 @@ def available_notification_actions():
}
]
+ if str(agent_id).isdigit():
+ action_types = get_notify_agents(return_dict=True).get(int(agent_id), {}).get('action_types', [])
+ if 'all' not in action_types:
+ actions = [a for a in actions if a['name'] in action_types]
+
return actions
def get_agent_class(agent_id=None, config=None):
if str(agent_id).isdigit():
- agent_id = int(agent_id)
-
- if agent_id == 0:
- return GROWL(config=config)
- elif agent_id == 1:
- return PROWL(config=config)
- elif agent_id == 2:
- return XBMC(config=config)
- elif agent_id == 3:
- return PLEX(config=config)
- elif agent_id == 6:
- return PUSHBULLET(config=config)
- elif agent_id == 7:
- return PUSHOVER(config=config)
- elif agent_id == 8:
- return OSX(config=config)
- elif agent_id == 9:
- return BOXCAR(config=config)
- elif agent_id == 10:
- return EMAIL(config=config)
- elif agent_id == 11:
- return TWITTER(config=config)
- elif agent_id == 12:
- return IFTTT(config=config)
- elif agent_id == 13:
- return TELEGRAM(config=config)
- elif agent_id == 14:
- return SLACK(config=config)
- elif agent_id == 15:
- return SCRIPTS(config=config)
- elif agent_id == 16:
- return FACEBOOK(config=config)
- elif agent_id == 17:
- return BROWSER(config=config)
- elif agent_id == 18:
- return JOIN(config=config)
- elif agent_id == 20:
- return DISCORD(config=config)
- elif agent_id == 21:
- return ANDROIDAPP(config=config)
- elif agent_id == 22:
- return GROUPME(config=config)
- elif agent_id == 23:
- return MQTT(config=config)
- elif agent_id == 24:
- return ZAPIER(config=config)
- elif agent_id == 25:
- return WEBHOOK(config=config)
- else:
- return Notifier(config=config)
+ agent = get_notify_agents(return_dict=True).get(int(agent_id), {}).get('class', Notifier)
+ return agent(config=config)
else:
return None
-def get_notify_agents():
+def get_notify_agents(return_dict=False):
+ if return_dict:
+ return {a['id']: a for a in available_notification_agents()}
return tuple(a['name'] for a in sorted(available_notification_agents(), key=lambda k: k['label']))
def get_notify_actions(return_dict=False):
if return_dict:
- return {a.pop('name'): a for a in available_notification_actions()}
+ return {a['name']: a for a in available_notification_actions()}
return tuple(a['name'] for a in available_notification_actions())
@@ -523,7 +533,7 @@ def add_notifier_config(agent_id=None, **kwargs):
% agent_id)
return False
- agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
+ agent = get_notify_agents(return_dict=True).get(agent_id, None)
if not agent:
logger.error("Tautulli Notifiers :: Unable to retrieve new notification agent: invalid agent_id %s."
@@ -572,7 +582,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
% agent_id)
return False
- agent = next((a for a in available_notification_agents() if a['id'] == agent_id), None)
+ agent = get_notify_agents(return_dict=True).get(agent_id, None)
if not agent:
logger.error("Tautulli Notifiers :: Unable to retrieve existing notification agent: invalid agent_id %s."
@@ -2368,6 +2378,190 @@ class PLEX(Notifier):
return config_option
+class PLEXMOBILEAPP(Notifier):
+ """
+ Plex Mobile App Notifications
+ """
+ NAME = 'Plex Android / iOS App'
+ NOTIFICATION_URL = 'https://notifications.plex.tv/api/v1/notifications'
+ _DEFAULT_CONFIG = {'user_ids': [],
+ 'tap_action': 'preplay',
+ }
+
+ def __init__(self, config=None):
+ super(PLEXMOBILEAPP, self).__init__(config=config)
+
+ self.configurations = {
+ 'created': {'group': 'media', 'identifier': 'tv.plex.notification.library.new'},
+ 'play': {'group': 'media', 'identifier': 'tv.plex.notification.playback.started'},
+ 'newdevice': {'group': 'admin', 'identifier': 'tv.plex.notification.device.new'}
+ }
+
+ def agent_notify(self, subject='', body='', action='', **kwargs):
+ if action not in self.configurations and not action.startswith('test'):
+ logger.error(u"Tautulli Notifiers :: Notification action %s not allowed for %s." % (action, self.NAME))
+ return
+
+ if action == 'test':
+ tests = []
+ for configuration in self.configurations:
+ tests.append(self.agent_notify(subject=subject, body=body, action='test_'+configuration))
+ return all(tests)
+
+ configuration_action = action.split('test_')[-1]
+
+ # No subject to always show up regardless of client selected filters
+ # icon can be info, warning, or error
+ # play = true to start playing when tapping the notification
+ # Send the minimal amount of data necessary through Plex servers
+ data = {
+ 'group': self.configurations[configuration_action]['group'],
+ 'identifier': self.configurations[configuration_action]['identifier'],
+ 'to': self.config['user_ids'],
+ 'data': {
+ 'provider': {
+ 'identifier': plexpy.CONFIG.PMS_IDENTIFIER,
+ 'title': plexpy.CONFIG.PMS_NAME
+ }
+ }
+ }
+
+ pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
+
+ if action.startswith('test'):
+ data['data']['player'] = {
+ 'title': 'Device',
+ 'platform': 'Platform',
+ 'machineIdentifier': 'Tautulli'
+ }
+ data['data']['user'] = {
+ 'title': 'User',
+ 'id': 0
+ }
+ data['metadata'] = {
+ 'type': 'movie',
+ 'title': subject,
+ 'year': body
+ }
+
+ elif action in ('play', 'newdevice'):
+ data['data']['player'] = {
+ 'title': pretty_metadata.parameters['player'],
+ 'platform': pretty_metadata.parameters['platform'],
+ 'machineIdentifier': pretty_metadata.parameters['machine_id']
+ }
+ data['data']['user'] = {
+ 'title': pretty_metadata.parameters['user'],
+ 'id': pretty_metadata.parameters['user_id'],
+ 'thumb': pretty_metadata.parameters['user_thumb'],
+ }
+
+ elif action == 'created':
+ # No addition data required for recently added
+ pass
+
+ else:
+ logger.error(u"Tautulli Notifiers :: Notification action %s not supported for %s." % (action, self.NAME))
+ return
+
+ if data['group'] == 'media' and not action.startswith('test'):
+ media_type = pretty_metadata.media_type
+ uri_rating_key = None
+
+ if media_type == 'movie':
+ metadata = {
+ 'type': media_type,
+ 'title': pretty_metadata.parameters['title'],
+ 'year': pretty_metadata.parameters['year'],
+ 'thumb': pretty_metadata.parameters['thumb']
+ }
+ elif media_type == 'show':
+ metadata = {
+ 'type': media_type,
+ 'title': pretty_metadata.parameters['show_name'],
+ 'thumb': pretty_metadata.parameters['thumb']
+ }
+ elif media_type == 'season':
+ metadata = {
+ 'type': 'show',
+ 'title': pretty_metadata.parameters['show_name'],
+ 'thumb': pretty_metadata.parameters['thumb'],
+ }
+ data['data']['count'] = pretty_metadata.parameters['episode_count']
+ elif media_type == 'episode':
+ metadata = {
+ 'type': media_type,
+ 'title': pretty_metadata.parameters['episode_name'],
+ 'grandparentTitle': pretty_metadata.parameters['show_name'],
+ 'index': pretty_metadata.parameters['episode_num'],
+ 'parentIndex': pretty_metadata.parameters['season_num'],
+ 'grandparentThumb': pretty_metadata.parameters['grandparent_thumb']
+ }
+ elif media_type == 'artist':
+ metadata = {
+ 'type': media_type,
+ 'title': pretty_metadata.parameters['artist_name'],
+ 'thumb': pretty_metadata.parameters['thumb']
+ }
+ elif media_type == 'album':
+ metadata = {
+ 'type': media_type,
+ 'title': pretty_metadata.parameters['album_name'],
+ 'year': pretty_metadata.parameters['year'],
+ 'parentTitle': pretty_metadata.parameters['artist_name'],
+ 'thumb': pretty_metadata.parameters['thumb'],
+ }
+ elif media_type == 'track':
+ metadata = {
+ 'type': 'album',
+ 'title': pretty_metadata.parameters['album_name'],
+ 'year': pretty_metadata.parameters['year'],
+ 'parentTitle': pretty_metadata.parameters['artist_name'],
+ 'thumb': pretty_metadata.parameters['parent_thumb']
+ }
+ uri_rating_key = pretty_metadata.parameters['parent_rating_key']
+ else:
+ logger.error(u"Tautulli Notifiers :: Media type %s not supported for %s." % (media_type, self.NAME))
+ return
+
+ data['metadata'] = metadata
+ data['uri'] = 'server://{}/com.plexapp.plugins.library/library/metadata/{}'.format(
+ plexpy.CONFIG.PMS_IDENTIFIER, uri_rating_key or pretty_metadata.parameters['rating_key']
+ )
+ data['play'] = self.config['tap_action'] == 'play'
+
+ headers = {'X-Plex-Token': plexpy.CONFIG.PMS_TOKEN}
+
+ return self.make_request(self.NOTIFICATION_URL, headers=headers, json=data)
+
+ def get_users(self):
+ user_ids = {u['user_id']: u['friendly_name'] for u in users.Users().get_users() if u['user_id']}
+ user_ids[''] = ''
+ return user_ids
+
+ def _return_config_options(self):
+ config_option = [{'label': 'Plex User(s)',
+ 'value': self.config['user_ids'],
+ 'name': 'plexmobileapp_user_ids',
+ 'description': 'Select which Plex User(s) to receive notifications. '
+ 'Note: The user(s) must have notifications enabled '
+ 'for the matching Tautulli triggers in their Plex mobile app.',
+ 'input_type': 'select',
+ 'select_options': self.get_users()
+ },
+ {'label': 'Notification Tap Action',
+ 'value': self.config['tap_action'],
+ 'name': 'plexmobileapp_tap_action',
+ 'description': 'Set the action when tapping on the notification.',
+ 'input_type': 'select',
+ 'select_options': {'preplay': 'Go to media pre-play screen',
+ 'play': 'Start playing the media'}
+ },
+ ]
+
+ return config_option
+
+
class PROWL(Notifier):
"""
Prowl notifications.
diff --git a/plexpy/plextv.py b/plexpy/plextv.py
index ada7d8f8..e2a38752 100644
--- a/plexpy/plextv.py
+++ b/plexpy/plextv.py
@@ -390,6 +390,14 @@ class PlexTV(object):
return request
+ def get_plextv_geoip(self, ip_address='', output_format=''):
+ uri = '/api/v2/geoip?ip_address=%s' % ip_address
+ request = self.request_handler.make_request(uri=uri,
+ request_type='GET',
+ output_format=output_format)
+
+ return request
+
def get_full_users_list(self):
own_account = self.get_plextv_user_details(output_format='xml')
friends_list = self.get_plextv_friends(output_format='xml')
@@ -936,3 +944,35 @@ class PlexTV(object):
"user_token": helpers.get_xml_attr(a, 'authToken')
}
return account_details
+
+ def get_geoip_lookup(self, ip_address=''):
+ if not ip_address or not helpers.is_public_ip(ip_address):
+ return
+
+ geoip_data = self.get_plextv_geoip(ip_address=ip_address, output_format='xml')
+
+ try:
+ xml_head = geoip_data.getElementsByTagName('location')
+ except Exception as e:
+ logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_geoip_lookup: %s." % e)
+ return None
+
+ for a in xml_head:
+ coordinates = helpers.get_xml_attr(a, 'coordinates').split(',')
+ latitude = longitude = None
+ if len(coordinates) == 2:
+ latitude, longitude = [helpers.cast_to_float(c) for c in coordinates]
+
+ geo_info = {"code": helpers.get_xml_attr(a, 'code') or None,
+ "country": helpers.get_xml_attr(a, 'country') or None,
+ "region": helpers.get_xml_attr(a, 'subdivisions') or None,
+ "city": helpers.get_xml_attr(a, 'city') or None,
+ "postal_code": helpers.get_xml_attr(a, 'postal_code') or None,
+ "timezone": helpers.get_xml_attr(a, 'time_zone') or None,
+ "latitude": latitude,
+ "longitude": longitude,
+ "continent": None, # keep for backwards compatibility with GeoLite2
+ "accuracy": None # keep for backwards compatibility with GeoLite2
+ }
+
+ return geo_info
diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py
index 52c14565..7fd8b506 100644
--- a/plexpy/pmsconnect.py
+++ b/plexpy/pmsconnect.py
@@ -2980,10 +2980,26 @@ class PmsConnect(object):
for a in xml_head:
server_response = {'mapping_state': helpers.get_xml_attr(a, 'mappingState'),
'mapping_error': helpers.get_xml_attr(a, 'mappingError'),
+ 'sign_in_state': helpers.get_xml_attr(a, 'signInState'),
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
- 'public_port': helpers.get_xml_attr(a, 'publicPort')
+ 'public_port': helpers.get_xml_attr(a, 'publicPort'),
+ 'private_address': helpers.get_xml_attr(a, 'privateAddress'),
+ 'private_port': helpers.get_xml_attr(a, 'privatePort')
}
+ if server_response['mapping_state'] == 'unknown':
+ server_response['reason'] = 'Plex remote access port mapping unknown'
+ elif server_response['mapping_state'] not in ('mapped', 'waiting'):
+ server_response['reason'] = 'Plex remote access port not mapped'
+ elif server_response['mapping_error'] == 'unreachable':
+ server_response['reason'] = 'Plex remote access port mapped, ' \
+ 'but the port is unreachable from Plex.tv'
+ elif server_response['mapping_error'] == 'publisherror':
+ server_response['reason'] = 'Plex remote access port mapped, ' \
+ 'but failed to publish the port to Plex.tv'
+ else:
+ server_response['reason'] = ''
+
return server_response
def get_update_staus(self):
diff --git a/plexpy/users.py b/plexpy/users.py
index d19bfe02..6afed298 100644
--- a/plexpy/users.py
+++ b/plexpy/users.py
@@ -21,7 +21,6 @@ from future.builtins import str
from future.builtins import object
import httpagentparser
-import time
import plexpy
if plexpy.PYTHON2:
diff --git a/plexpy/version.py b/plexpy/version.py
index 3538f031..2e3f9164 100644
--- a/plexpy/version.py
+++ b/plexpy/version.py
@@ -18,4 +18,4 @@
from __future__ import unicode_literals
PLEXPY_BRANCH = "python3"
-PLEXPY_RELEASE_VERSION = "v2.2.2-beta"
+PLEXPY_RELEASE_VERSION = "v2.2.3-beta"
diff --git a/plexpy/versioncheck.py b/plexpy/versioncheck.py
index a0f1958d..17b6c2c4 100644
--- a/plexpy/versioncheck.py
+++ b/plexpy/versioncheck.py
@@ -147,8 +147,8 @@ def getVersion():
return current_version, 'origin', current_branch
-def check_update(auto_update=False, notify=False):
- check_github(auto_update=auto_update, notify=notify)
+def check_update(scheduler=False, notify=False):
+ check_github(scheduler=scheduler, notify=notify)
if not plexpy.CURRENT_VERSION:
plexpy.UPDATE_AVAILABLE = None
@@ -171,7 +171,7 @@ def check_update(auto_update=False, notify=False):
plexpy.WIN_SYS_TRAY_ICON.update(icon=icon, hover_text=hover_text)
-def check_github(auto_update=False, notify=False):
+def check_github(scheduler=False, notify=False):
plexpy.COMMITS_BEHIND = 0
if plexpy.CONFIG.GIT_TOKEN:
@@ -248,7 +248,7 @@ def check_github(auto_update=False, notify=False):
'plexpy_update_commit': plexpy.LATEST_VERSION,
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
- if auto_update and not plexpy.DOCKER:
+ if scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and not plexpy.DOCKER:
logger.info('Running automatic update.')
plexpy.shutdown(restart=True, update=True)
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index 964df164..f6de0c2f 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -1936,6 +1936,10 @@ class WebInterface(object):
}
```
"""
+ # For backwards compatibility
+ if 'id' in kwargs:
+ row_id = kwargs['id']
+
data_factory = datafactory.DataFactory()
stream_data = data_factory.get_stream_details(row_id, session_key)
@@ -2993,6 +2997,7 @@ class WebInterface(object):
"notify_recently_added_delay": plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY,
"notify_concurrent_by_ip": checked(plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP),
"notify_concurrent_threshold": plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD,
+ "notify_continued_session_threshold": plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD,
"home_sections": json.dumps(plexpy.CONFIG.HOME_SECTIONS),
"home_stats_cards": json.dumps(plexpy.CONFIG.HOME_STATS_CARDS),
"home_library_cards": json.dumps(plexpy.CONFIG.HOME_LIBRARY_CARDS),
@@ -3024,11 +3029,7 @@ class WebInterface(object):
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR,
- "win_sys_tray": checked(plexpy.CONFIG.WIN_SYS_TRAY),
- "maxmind_license_key": plexpy.CONFIG.MAXMIND_LICENSE_KEY,
- "geoip_db": plexpy.CONFIG.GEOIP_DB,
- "geoip_db_installed": plexpy.CONFIG.GEOIP_DB_INSTALLED,
- "geoip_db_update_days": plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS
+ "win_sys_tray": checked(plexpy.CONFIG.WIN_SYS_TRAY)
}
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
@@ -3260,36 +3261,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Database backup failed.'}
- @cherrypy.expose
- @cherrypy.tools.json_out()
- @requireAuth(member_of("admin"))
- @addtoapi()
- def install_geoip_db(self, update=False, **kwargs):
- """ Downloads and installs the GeoLite2 database """
-
- update = helpers.bool_true(update)
-
- result = helpers.install_geoip_db(update=update)
-
- if result:
- return {'result': 'success', 'message': 'GeoLite2 database installed successful.', 'updated': result}
- else:
- return {'result': 'error', 'message': 'GeoLite2 database install failed.', 'updated': 0}
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- @requireAuth(member_of("admin"))
- @addtoapi()
- def uninstall_geoip_db(self, **kwargs):
- """ Uninstalls the GeoLite2 database """
-
- result = helpers.uninstall_geoip_db()
-
- if result:
- return {'result': 'success', 'message': 'GeoLite2 database uninstalled successfully.'}
- else:
- return {'result': 'error', 'message': 'GeoLite2 database uninstall failed.'}
-
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@@ -5807,7 +5778,7 @@ class WebInterface(object):
@requireAuth()
@addtoapi()
def get_geoip_lookup(self, ip_address='', **kwargs):
- """ Get the geolocation info for an IP address. The GeoLite2 database must be installed.
+ """ Get the geolocation info for an IP address.
```
Required parameters:
@@ -5818,7 +5789,7 @@ class WebInterface(object):
Returns:
json:
- {"continent": "North America",
+ {"code": 'US",
"country": "United States",
"region": "California",
"city": "Mountain View",
@@ -5828,15 +5799,24 @@ class WebInterface(object):
"longitude": -122.0838,
"accuracy": 1000
}
- json:
- {"error": "The address 127.0.0.1 is not in the database."
- }
```
"""
- geo_info = helpers.geoip_lookup(ip_address)
- if isinstance(geo_info, str):
- return {'error': geo_info}
- return geo_info
+ message = ''
+ if not ip_address:
+ message = 'No IP address provided.'
+ elif not helpers.is_valid_ip(ip_address):
+ message = 'Invalid IP address provided: %s' % ip_address
+ elif not helpers.is_public_ip(ip_address):
+ message = 'Non-public IP address provided: %s' % ip_address
+
+ if message:
+ return {'result': 'error', 'message': message}
+
+ plex_tv = plextv.PlexTV()
+ geo_info = plex_tv.get_geoip_lookup(ip_address)
+ if geo_info:
+ return {'result': 'success', 'data': geo_info}
+ return {'result': 'error', 'message': 'Failed to lookup GeoIP info for address: %s' % ip_address}
@cherrypy.expose
@cherrypy.tools.json_out()