diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..7f2945fa --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,22 @@ +#### Branch: The name of the branch (e.g. master or develop) +#### Branch version commit hash: (See the About page) + + +Expectation | Result +------------ | ------------- +Your expectation paragraph(s) text here. Before the bar >> | then, your result paragraph(s) text here. + + +**Steps to reproduce:** +*1)* Step to take +*2)* Step to take +... + + +Either a link to gist (best as they are easily deleted) or log text wrapped in "pre" tags + + +Additional notes: +For example, any relevant config settings that may be help us replicate the reported issue + +Targeting your info so that the issue can be reproduced is the single fastest way to get it resolved diff --git a/CHANGES.md b/CHANGES.md index a2906832..906b3ff1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,373 @@ -### 0.12.0 (2016-12-19 03:00:00 UTC) +### 0.13.0 (2017-12-06 12:40:00 UTC) + +* Change don't fetch caps for disabled nzb providers +* Change recent search to use centralised title and URL parser for newznab +* Add display unaired season 1 episodes of a new show in regular and pro I view modes +* Change improve page load time when loading images +* Update isotope library 2.2.2 to 3.0.1 +* Add lazyload package 3.0.0 (2e318b1) +* Add webencodings 0.5 (3970651) to assist parsing legacy web content +* Change improve add show search results by comparing search term to an additional unidecoded result set +* Change webserver startup to correctly use xheaders in reverse proxy or load balance set-ups +* Update backports_abc 0.4 to 0.5 +* Update Beautiful Soup 4.4.0 (r397) to 4.6.0 (r449) +* Update cachecontrol library 0.11.5 to 0.12.3 (db54c40) +* Update Certifi 2015.11.20.1 (385476b) to 2017.07.27 (f808089) +* Update chardet packages 2.3.0 (d7fae98) to 3.0.4 (9b8c5c2) +* Update dateutil library 2.4.2 (d4baf97) to 2.6.1 (2f3a160) +* Update feedparser library 5.2.0 (8c62940) to 5.2.1 (f1dd1bb) +* Update html5lib 0.99999999/1.0b9 (46dae3d) to (1a28d72) +* Update IMDb 5.1dev20160106 to 5.1 (r907) +* Update moment.js 2.15.1 to 2.17.1 +* Update PNotify library 2.1.0 to 3.0.0 (175af26) +* Update profilehooks 1.8.2.dev0 (ee3f1a8) to 1.9.0 (de7d59b) +* Update rarfile to 3.0 (3e54b22) +* Update Requests library 2.9.1 (a1c9b84) to 2.13.0 (fc54869) +* Update SimpleJSON library 3.8.1 (6022794) to 3.10.0 (c52efea) +* Update Six compatibility library 1.10.0 (r405) to 1.10.0 (r433) +* Update socks from SocksiPy 1.0 to PySocks 1.6.5 (b4323df) +* Update Tornado Web Server 4.5.dev1 (92f29b8) to 4.5.1 (79b2683) +* Update unidecode library 0.04.18 to 0.04.21 (e99b0e3) +* Update xmltodict library 0.9.2 (eac0031) to 0.10.2 (375d3a6) +* Update Bootstrap 3.2.0 to 3.3.7 +* Update Bootstrap Hover Dropdown 2.0.11 to 2.2.1 +* Update imagesloaded 3.1.8 to 4.1.1 +* Update jquery.cookie 1.0 (21349d9) to JS-Cookie 2.1.3 (c1aa987) +* Update jquery.cookiejar 1.0.1 to 1.0.2 +* Update jQuery JSON 2.2 (c908771) to 2.6 (2339804) +* Update jquery.form plugin 3.35.0 to 3.51.0 (6bf24a5) +* Update jQuery SelectBoxes 2.2.4 to 2.2.6 +* Update jquery-tokeninput 1.60 to 1.62 (9c36e19) +* Update jQuery-UI 1.10.4 to 1.12.1 - minimum supported IE is 8 +* Update jQuery UI Touch Punch 0.2.2 to 0.2.3 +* Update qTip 2.2.1 to 2.2.2 +* Update tablesorter 2.17.7 to 2.28.5 +* Update jQuery 1.8.3 to 2.2.4 +* Add one time run to start up that deletes troublemaking compiled files +* Fix reload of homepage after restart in some browsers +* Add detection of '1080p Remux' releases as fullhdbluray +* Add "Perform search tasks" to Config/Media Providers/Options +* Change improve clarity of enabled providers on Config/Media Providers +* Add option to limit WebDL propers to original release group under Config/Search/Media Search +* Change add IPv4 config option when enabling IPv6. +* Add autoProcessTV/onTxComplete.bat to improve Windows clients Deluge, qBittorrent, Tranmission, and uTorrent +* Add Blutopia torrent provider +* Add MagnetDL torrent provider +* Add SceneHD torrent provider +* Add Skytorrents torrent provider +* Add TorrentVault torrent provider +* Add WorldOfP2P torrent provider +* Change do not have shows checked by default on import page. To re-enable import shows checked by default, + 1) On config page 'Save' 2) Stop SG 3) Find 'import_default_checked_shows' in config.ini and set '1' 4) Start SG +* Add Nyaa (.si) torrent provider +* Add Trakt watchlist to Add show/Trakt Cards +* Change revoke application access at Trakt when account is deleted in SG +* Add persistent hide/unhide cards to Add show/Trakt and Add show/IMDb Cards +* Change simplify dropdowns at all Add show/Cards +* Change cosmetic title on shutdown +* Change use TVDb API v2 +* Change improve search for PROPERS +* Change catch show update task errors +* Change simplify and update FreeBSD init script +* Change only use newznab Api key if needed +* Change editshow saving empty scene exceptions +* Change improve TVDB data handling +* Change improve post processing by using more snatch history data +* Change show update, don't delete any ep in DB if eps are not returned from indexer +* Change prevent unneeded error message during show update +* Change improve performance, don't fetch episode list when retrieving a show image +* Change don't remove episodes from DB with status: SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, DOWNLOADED, ARCHIVED, IGNORED +* Change add additional episode removal protections for TVDb_api v2 +* Change filter SKIPPED items from episode view +* Change improve clarity of various error message by including relevant show name +* Change extend WEB PROPER release group check to ignore SD releases +* Change increase performance by reducing TVDb API requests with a global token +* Change make indexer lookup optional in NameParser, and deactivate during searches +* Change improve newnab autoselect categories +* Change add nzb.org BoxSD and BoxHD categories +* Change post processor, ignore symlinks found in process_dir +* Change file modify date of episodes older than 1970 can be changed to airdate, log warning on set fail +* Add new parameter 'poster' to indexer api +* Add optional tvdb_api load season image: lINDEXER_API_PARMS['seasons'] = True +* Add optional tvdb_api load season wide image: lINDEXER_API_PARMS['seasonwides'] = True +* Add Fuzzywuzzy 0.15.1 to sort search results +* Change remove search results filtering from tv info source +* Change suppress startup warnings for Fuzzywuzzy and Cheetah libs +* Change show search, add options to choose order of search results +* Add option to sort search results by 'A to Z' or 'First aired' +* Add option to sort search results by 'Relevancy' using Fuzzywuzzy lib +* Change search result anchor text uses SORT_ARTICLE setting for display +* Change existing shows in DB are no longer selectable in result list +* Change add image to search result item hover over +* Change improve image load speed on browse Trakt/IMDb/AniDB pages +* Add a changeable master Show ID when show no longer found at TV info source due to an ID change +* Add guiding links to assist user to change TV Info Source ID +* Add "Shows with abandoned master IDs" to Manage/Show Processes Page to link shows that can have their show IDs + adjusted in order to sustain TV info updates +* Add "Shows from defunct TV info sources" to Manage/Show Processes page to link shows that can be switched to a + different default TV info source +* Add shows not found at a TV info source for over 7 days will only be retried once a week +* Change prevent showing 'Mark download as bad and retry?' dialog when status doesn't require it +* Add warn icon indicator of abandoned IDs to "Manage" menu bar and "Manage/Show Processes" menu item +* Add shows that have no replacement ID can be ignored at "Manage/Show Processes", the menu bar warn icon hides if all are ignored +* Change FreeBSD initscript to use command_interpreter +* Add Slack notifier to Notifications config/Social +* Change allow Cheetah template engine version 2 and newer +* Change improve handling of relative download links from providers +* Change enable TorrentBytes provider +* Change after SG is updated, don't attempt to send a Plex client notifications if there is no client host set +* Add file name to possible names in history lookup post processing +* Add garbage name handling to name parser +* Change overhaul Notifications, add Notifier Factory and DRY refactoring +* Notifiers are now loaded into memory on demand +* Add bubble links to Notifications config tabs +* Add Discordapp notifier to Notifications config/Social +* Add Gitter notifier to Notifications config/Social +* Change order of notifiers in Notifications config tabs +* Remove Pushalot notifier +* Remove XBMC notifier +* Change a link to include webroot for "plot overview for this ended show" +* Change Bulk Changes and Notifications save to be web_root setting aware +* Change subtitle addons no longer need to be saved before Search Subtitles is enabled as a + forbidden action to reuse an exited FindSubtitles thread is no longer attempted +* Fix tools menu not opening for some browsers +* Change overhaul handling of PROPERS/REPACKS/REAL +* Add restriction to allow only same release group for repacks +* Change try all episode names with 'real', 'repack', 'proper' +* Add tip to search settings/media search about improved matching with optional regex library +* Change use value of "Update shows during hour" in General Settings straight after it is saved instead of after restart +* Change add tips for what to use for Growl notifications on Windows +* Change if a newly added show is not found on indexer, remove already created empty folder +* Change parse 1080p Bluray AVC/VC1 to a quality instead of unknown +* Add quality tag to archived items, improve displayShow/"Change selected episodes to" +* Use to prevent "Update to" on those select episodes while preserving the downloaded quality +* Change group "Downloaded" status qualities into one section +* Add "Downloaded/with archived quality" to set shows as downloaded using quality of archived status +* Add "Archived with/downloaded quality" to set shows as archived using quality of downloaded status +* Add "Archived with/default (min. initial quality of show here)" +* Change when settings/Post Processing/File Handling/Status of removed episodes/Set Archived is enabled, set status and quality accordingly +* Add downloaded and archived statuses to Manage/Episode Status +* Add quality pills to Manage/Episode Status +* Change Manage/Episode Status season output format to be more readable + + +### 0.12.37 (2017-11-12 10:35:00 UTC) + +* Change improve .nzb handling + + +### 0.12.36 (2017-11-01 11:45:00 UTC) + +* Change qBittorent to handle the change to its API success/fail response + + +### 0.12.35 (2017-10-27 20:30:00 UTC) + +* Change and add some network logos + + +### 0.12.34 (2017-10-25 15:20:00 UTC) + +* Change improve TVChaos parser + + +### 0.12.33 (2017-10-12 13:00:00 UTC) + +* Change improve handling of torrent auth failures + + +### 0.12.32 (2017-10-11 02:05:00 UTC) + +* Change improve PA torrent access + + +### 0.12.31 (2017-10-06 22:30:00 UTC) + +* Change improve handling of connection failures for metadata during media processing + + +### 0.12.30 (2017-09-29 00:20:00 UTC) + +* Fix Media Providers/Custom Newznab tab action 'Delete' then 'Save Changes' +* Fix enforce value API expects for paused show flag + + +### 0.12.29 (2017-09-17 09:00:00 UTC) + +* Fix provider nCore +* Change .torrent checker due to files created with qB 3.3.16 (affects nCore and NBL) + + +### 0.12.28 (2017-08-26 18:15:00 UTC) + +* Change prevent indexer specific release name parts from fudging search logic + + +### 0.12.27 (2017-08-22 19:00:00 UTC) + +* Update to UnRar 5.50 release + + +### 0.12.26 (2017-08-20 13:05:00 UTC) + +* Fix infinite loop loading network_timezones +* Change add optional "stack_size" setting as integer to config.ini under "General" stanza +* Change prevent too many retries when loading network timezones, conversions, and zoneinfo in a short time +* Update to UnRar 5.50 beta 6 + + +### 0.12.25 (2017-06-19 23:35:00 UTC) + +* Remove provider SceneAccess + + +### 0.12.24 (2017-07-31 20:42:00 UTC) + +* Fix copy post process method on posix + + +### 0.12.23 (2017-07-18 16:55:00 UTC) + +* Remove obsolete tvrage_api lib + + +### 0.12.22 (2017-07-13 20:20:00 UTC) + +* Fix "Server failed to return anything useful" when should be using cached .torrent file +* Fix displayShow 'Unaired' episode rows change state where appropriate +* Change displayShow to stop requiring an airdate for checkboxes + + +### 0.12.21 (2017-06-19 23:35:00 UTC) + +* Change provider Bit-HDTV user/pass to cookie + + +### 0.12.20 (2017-06-14 22:00:00 UTC) + +* Change send info now required by qBittorrent 3.13+ clients + + +### 0.12.19 (2017-05-20 10:30:00 UTC) + +* Remove provider Freshon.tv + + +### 0.12.18 (2017-05-15 23:00:00 UTC) + +* Change thexem, remove tvrage from xem + + +### 0.12.17 (2017-05-15 22:10:00 UTC) + +* Remove provider ExtraTorrent +* Change thexem tvrage mappings are deprecated, data fetch disabled + + +### 0.12.16 (2017-05-05 16:40:00 UTC) + +* Fix multiple SpeedCD cookie + + +### 0.12.15 (2017-05-04 00:40:00 UTC) + +* Remove provider Nyaa +* Change improve RSS validation (particularly for anime) +* Change improve support for legacy magnet encoding + + +### 0.12.14 (2017-05-02 17:10:00 UTC) + +* Change provider Transmithe.net is now Nebulance + + +### 0.12.13 (2017-04-23 18:50:00 UTC) + +* Change add filter for thetvdb show overview +* Change remove SpeedCD 'inspeed_uid' cookie requirement + + +### 0.12.12 (2017-03-30 03:15:00 UTC) + +* Change search of SpeedCD, TVChaos and parse of TorrentDay + + +### 0.12.11 (2017-03-17 02:00:00 UTC) + +* Change SpeedCD to cookie auth as username/password is not reliable +* Change Usenet-Crawler media provider icon + + +### 0.12.10 (2017-03-12 16:00:00 UTC) + +* Change refactor client for Deluge 1.3.14 compatibility +* Change ensure IPT authentication is valid before use + + +### 0.12.9 (2017-02-24 18:40:00 UTC) + +* Fix issue saving custom NewznabProviders + + +### 0.12.8 (2017-02-19 13:50:00 UTC) + +* Change BTN API hostname + + +### 0.12.7 (2017-02-17 15:00:00 UTC) + +* Change accept lists in JSON responses +* Change do not log error for empty BTN un/pw in most cases +* Change BTN to only try API once when doing alternative name searches +* Change when API fails, warn users as a tip that they can configure un/pw + + +### 0.12.6 (2017-02-17 03:48:00 UTC) + +* Change skip episodes that have no wanted qualities +* Change download picked .nzb file on demand and not before +* Change improve provider title processing +* Change improve handling erroneous JSON responses +* Change improve find show with unicode characters +* Change improve results for providers Omgwtf, SpeedCD, Transmithenet, Zoogle +* Change validate .torrent files that contain optional header data +* Fix case where an episode status was not restored on failure +* Add raise log error if no wanted qualities are found +* Change add un/pw to Config/Media providers/Options for BTN API graceful fallback (can remove Api key for security) +* Change only download torrent once when using blackhole +* Add Cloudflare module 1.6.8 (be0a536) to handle specific CF connections +* Add Js2Py 0.43 (c1442f1) Cloudflare dependency +* Add pyjsparser 2.4.5 (cd5b829) Js2Py dependency +* Remove Torrentshack + + +### 0.12.5 (2017-01-16 16:22:00 UTC) + +* Change TD search URL +* Fix saving Media Providers when either Search NZBs/Torrents is disabled + + +### 0.12.4 (2016-12-31 00:50:00 UTC) + +* Remove Wombles nzb provider + + +### 0.12.3 (2016-12-27 15:20:00 UTC) + +* Add UK date format handling to name parser + + +### 0.12.2 (2016-12-20 16:00:00 UTC) + +* Change Rarbg and IPT urls + + +### 0.12.1 (2016-12-19 12:00:00 UTC) + +* Fix image scan log for show titles that contain "%" + + +### 0.12.0 (2016-12-19 03:00:00 UTC) * Add strict Python version check (equal to, or higher than 2.7.9 and less than 3.0), **exit** if incorrect version * Update unidecode library 0.04.11 to 0.04.18 (fd57cbf) diff --git a/HACKS.txt b/HACKS.txt index 6780f8cd..832210ea 100644 --- a/HACKS.txt +++ b/HACKS.txt @@ -1,13 +1,14 @@ Libs with customisations... -/lib/cachecontrol/caches/file_cache.py /lib/dateutil/zoneinfo/__init__.py +/lib/dateutil/tz/tz.py /lib/hachoir_core/config.py /lib/hachoir_core/stream/input_helpers.py /lib/hachoir_metadata/jpeg.py /lib/hachoir_metadata/metadata.py /lib/hachoir_metadata/riff.py /lib/hachoir_parser/guess.py +/lib/hachoir_parser/misc/torrent.py /lib/lockfile/mkdirlockfile.py /lib/pynma/pynma.py /lib/requests/packages/urllib3/connectionpool.py diff --git a/SickBeard.py b/SickBeard.py index 85d22764..0a2d3f8c 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -32,16 +32,25 @@ import shutil import subprocess import time import threading +import warnings + +warnings.filterwarnings('ignore', module=r'.*fuzzywuzzy.*') +warnings.filterwarnings('ignore', module=r'.*Cheetah.*') if not (2, 7, 9) <= sys.version_info < (3, 0): print('Python %s.%s.%s detected.' % sys.version_info[:3]) print('Sorry, SickGear requires Python 2.7.9 or higher. Python 3 is not supported.') sys.exit(1) +try: + import _cleaner +except (StandardError, Exception): + pass + try: import Cheetah - if Cheetah.Version[0] != '2': + if Cheetah.Version[0] < '2': raise ValueError except ValueError: print('Sorry, requires Python module Cheetah 2.1.0 or newer.') @@ -302,6 +311,17 @@ class SickGear(object): print(u'Unable to find "%s", all settings will be default!' % sickbeard.CONFIG_FILE) sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) + stack_size = None + try: + stack_size = int(sickbeard.CFG['General']['stack_size']) + except: + stack_size = None + + if stack_size: + try: + threading.stack_size(stack_size) + except (StandardError, Exception) as e: + print('Stack Size %s not set: %s' % (stack_size, e.message)) # check all db versions for d, min_v, max_v, mo in [ @@ -359,10 +379,7 @@ class SickGear(object): if sickbeard.WEB_HOST and sickbeard.WEB_HOST != '0.0.0.0': self.webhost = sickbeard.WEB_HOST else: - if sickbeard.WEB_IPV6: - self.webhost = '::' - else: - self.webhost = '0.0.0.0' + self.webhost = (('0.0.0.0', '::')[sickbeard.WEB_IPV6], '')[sickbeard.WEB_IPV64] # web server options self.web_options = { @@ -382,7 +399,8 @@ class SickGear(object): # start web server try: # used to check if existing SG instances have been started - sickbeard.helpers.wait_for_free_port(self.web_options['host'], self.web_options['port']) + sickbeard.helpers.wait_for_free_port( + sickbeard.WEB_IPV6 and '::1' or self.web_options['host'], self.web_options['port']) self.webserver = WebServer(self.web_options) self.webserver.start() @@ -420,9 +438,15 @@ class SickGear(object): startup_background_tasks = threading.Thread(name='FETCH-XEMDATA', target=sickbeard.scene_exceptions.get_xem_ids) startup_background_tasks.start() - # sure, why not? + # check history snatched_proper update + if not db.DBConnection().has_flag('history_snatch_proper'): + # noinspection PyUnresolvedReferences + history_snatched_proper_task = threading.Thread(name='UPGRADE-HISTORY-ACTION', + target=sickbeard.history.history_snatched_proper_fix) + history_snatched_proper_task.start() + if sickbeard.USE_FAILED_DOWNLOADS: - failed_history.trimHistory() + failed_history.remove_old_history() # Start an update if we're supposed to if self.force_update or sickbeard.UPDATE_SHOWS_ON_START: diff --git a/_cleaner.py b/_cleaner.py new file mode 100644 index 00000000..1cac221f --- /dev/null +++ b/_cleaner.py @@ -0,0 +1,33 @@ +# remove this file when no longer needed + +import os +import shutil + +parent_dir = os.path.abspath(os.path.dirname(__file__)) +cleaned_file = os.path.abspath(os.path.join(parent_dir, r'.cleaned.tmp')) +if not os.path.isfile(cleaned_file): + dead_dirs = [os.path.abspath(os.path.join(parent_dir, *d)) for d in [ + ('tornado',), + ('lib', 'feedcache'), + ('lib', 'jsonrpclib'), + ('lib', 'shove'), + ('lib', 'unrar2') + ]] + + for dirpath, dirnames, filenames in os.walk(parent_dir): + for dead_dir in filter(lambda x: x in dead_dirs, [os.path.abspath(os.path.join(dirpath, d)) for d in dirnames]): + try: + shutil.rmtree(dead_dir) + except (StandardError, Exception): + pass + + for filename in [fn for fn in filenames if os.path.splitext(fn)[-1].lower() in ('.pyc', '.pyo')]: + try: + os.remove(os.path.abspath(os.path.join(dirpath, filename))) + except (StandardError, Exception): + pass + + with open(cleaned_file, 'wb') as fp: + fp.write('This file exists to prevent a rerun delete of *.pyc, *.pyo files') + fp.flush() + os.fsync(fp.fileno()) diff --git a/autoProcessTV/autoProcessTV.py b/autoProcessTV/autoProcessTV.py index adf1e0d8..0ea32186 100755 --- a/autoProcessTV/autoProcessTV.py +++ b/autoProcessTV/autoProcessTV.py @@ -109,6 +109,8 @@ def processEpisode(dir_to_process, org_NZB_name=None, status=None): params = {} + params['is_basedir'] = 0 + params['quiet'] = 1 params['dir'] = dir_to_process diff --git a/autoProcessTV/onTxComplete.bat b/autoProcessTV/onTxComplete.bat new file mode 100644 index 00000000..fed73539 --- /dev/null +++ b/autoProcessTV/onTxComplete.bat @@ -0,0 +1,321 @@ +@ECHO OFF +GOTO :main +******************************************************************************* + +onTxComplete.bat v1.0 for Sickgear + + Script to copy select files to a location for SickGear to post process. + + This allows the 'Move' post process episode method to be used so that + seeding files are not post processed over and over. + +******************************************************************************* + +Supported clients +----------------- +* Deluge clients 1.3.15 and newer clients +* qBittorrent 3.3.12 and newer clients +* Transmission 2.84 and newer clients +* uTorrent 2.2.1 and newer clients + + +How this works +-------------- +Completed downloads are copied from a seeding location to an isolated location. +SG will 'Move' processed content from isolation to the final show location. + +The four parameters; +param1 = Isolation path where to copy completed downloads for SG to process. + This value *must* .. + 1) be different to the path where the client saves completed downloads + 2) be configured in SG (see later, step a) + This path should be created on first run, if not, create it yourself. + +param2 = This filter value is compared with either a client set label, or the + tail of the path where the client downloaded seeding file is located. + Every download is skipped except those that compare successfully. + +param3 = Client set downloaded item category or label (e.g. "%L") + +param4 = Client set downloaded item content path (e.g. "%F", "%D\%F" etc) + +The values of params 3 and 4 can be found documented in the download client or +at the client webiste. Other clients may be able to replace param3 and param4 +to fit (see examples). + + +To set up SickGear +------------------ +a) Set /config/postProcessing/Post Processing -> "Completed TV downloads" + .. to match param1 + +b) Set /config/postProcessing/Post Processing -> "Process episode method" + .. to 'Move' + +c) Enable /config/postProcessing/Post Processing -> "Scan and post process" + +d) Enable /config/postProcessing/Post Processing -> "Postpone post processing" + +e) Set /config/search/Torrent Results -> "Set torrent label/category" + If using "Black hole" method or if there is no label field, then you must use + client auto labeller or a torrent completed save path that ends with param2, + for Transmission, see note(2) in that section below. + + +To set up the download client +----------------------------- + +For Deluge +---------- +Deluge clients call scripts with fixed params that prevent passing params, +rename onTxComplete.sample.cfg as onTxComplete.cfg and edit parameters there. + +A Deluge label is used to isolate SG downloads from everything else. + +1) Enable the "Label" plugin + +2) Enable the "Execute" plugin + +3) In the "Execute" plugin settings /Add Command/Event/Torrent Complete + set command to ... [script dir]\onTxComplete.bat + +4) Add the label set in step (e) to Deluge, right click the label and select + "Label Options"/Location/Move completed to/Other/ .. choose a folder created + where its name is identical to this label e.g. [path]\[label], (F:\Files\SG) + +Reference: http://dev.deluge-torrent.org/wiki/Plugins/Execute + + +For qBittorrent +--------------- +The above four parameters are used to configure and run the script. + +Use one cmd from below replacing "[script dir]" with the path to this script + +Set Options/Downloads/Run an external program on torrent completion + +.. to run in windowless mode (the normal run mode used) + cmd /c start "" /B [script dir]\onTxComplete.bat "F:\sg_pp" "SG" "%L" "%F" + +OR .. +.. to run in console window mode (test mode to see console output) + cmd /c start "" /min [script dir]\onTxComplete.bat "F:\sg_pp" "SG" "%L" "%F" + + +For Transmission +---------------- +Transmission clients call scripts with fixed params that prevent passing params, +rename onTxComplete.sample.cfg as onTxComplete.cfg and edit parameters there. + +Transmission does not contain labels, instead, the set path(2) is compared +to the config file param2 value to isolate SG downloads from everything else. + +1) Edit/Preferences/Downloading/Call script when torrent is completed + ... Navigate to this script location and select it + +2) Follow "To set up SickGear" instruction but at step (e), + set "Downloaded files location" to a created folder with name ending in the + config file value of param2 e.g. [path]\[param2] + +Reference: https://trac.transmissionbt.com/wiki/Scripts + + +For uTorrent +------------ +The above four parameters are used to configure and run the script. + +Use one cmd below replacing "[script dir]" with the path to this script + +1) Set Preferences/Advanced/Run program/Run this program when a torrent finishes + cmd /c start "" /B [script dir]\onTxComplete.bat "F:\sg_pp" "SG" "%L" "%D\%F" + +It is advised to not use the uTorrent "Move completed downloads" feature because +it runs scripts before move actions complete, bad. Consider switching. + +Reference: https://stackoverflow.com/a/29071224 + +:main +rem *************************************************************************** +rem Set 1 to enable test mode output (default: blank) +SET testmode= +rem *************************************************************************** +SETLOCAL +SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION + +rem Get install dir stripped of trailing slash +SET "install_dir=%~dp0" + +IF "" NEQ "%~4" ( + + rem Use the four input parameters + rem This also strips quotes used to safely pass values containing spaces + SET "sg_path=%~1" + SET "sg_label=%~2" + SET "client_label=%~3" + SET "content_path=%~4" + SET "check_label_path_tail=" + +) ELSE ( + + rem Process config file + SET "cfgfile=!install_dir!onTxComplete.cfg" + FOR /F "eol=; tokens=1,2 delims==" %%a IN (!cfgfile!) DO ( + SET "%%a=%%b" + IF "1" == "!testmode!" ( + ECHO Config ... %%a = %%b + ) + ) + + SET "nullvar=" + IF NOT DEFINED param1 SET "nullvar=1" + IF NOT DEFINED param2 SET "nullvar=1" + IF DEFINED nullvar ( + ECHO Error: Issue while reading file !cfgfile! + GOTO:exit + ) + SET "sg_path=!param1!" + SET "sg_label=!param2!" + + rem Attempt to read Transmision environment variables + SET "client_name=%TR_TORRENT_NAME%" + SET "client_path=%TR_TORRENT_DIR%" + + SET "nullvar=" + IF "" == "!client_name!" SET "nullvar=1" + IF "" == "!client_path!" SET "nullvar=1" + + IF DEFINED nullvar ( + + rem With no Transmission vars, attempt to read input parameters from Deluge + IF "" == "%~3" ( + + ECHO Error: %0 not enough input params, Deluge sends id, name, and path + GOTO :exit + + ) + + rem Deluge input parameters (i.e. "TorrentID" "Torrent Name" "Torrent Path") + rem This also strips quotes used to safely pass values containing spaces + SET "client_name=%~2" + SET "client_path=%~3" + + ) + + SET "content_path=!client_path!\!client_name!" + SET "check_label_path_tail=1" +) + + +rem Replace any double slashes in path with single slash +SET "sg_path=!sg_path:\\=\!" +SET "content_path=!content_path:\\=\!" + +rem Remove long path switch for most compatiblity, newer OSes may omit this +IF "\?\" == "!sg_path:~0,3!" SET "sg_path=!sg_path:~3!" +IF "\?\" == "!content_path:~0,3!" SET "content_path=!content_path:~3!" + +rem Remove any trailing slashes from paths +IF "\" == "!sg_path:~-1!" SET "sg_path=!sg_path:~0,-1!" +IF "\" == "!content_path:~-1!" SET "content_path=!content_path:~0,-1!" + + +IF DEFINED check_label_path_tail ( + + rem Enable the copy action if path ends with user defined label + + SET "client_label=!sg_label!" + +:loop -- label strlen + IF NOT "" == "!sg_label:~%len%!" SET /A len+=1 & GOTO :loop + SET /A len+=1 + + IF "\!sg_label!" NEQ "!client_path:~-%len%!" SET "client_label=skip copy" + +) + + +rem Create ".!sync" filename +SET "syncext=^!sync" +SET "syncfile=!sg_path!\copying.!syncext!" + + +IF "1" == "!testmode!" ( + + ECHO Running in ***test mode*** - files will not be copied + ECHO param1 = !sg_path! + ECHO param2 = !sg_label! + ECHO param3 = !client_label! + ECHO param4 = !content_path! + ECHO !syncfile! + +) + + +CALL:StartsWith "!client_label!" "!sg_label!" && ( + + IF NOT EXIST "!sg_path!" MKDIR "!sg_path!" + + IF EXIST "!sg_path!" ( + + rem Determine file/folder as these need to be handled differently + SET attr=%~a4 + IF /I "dir" == "!attr:~0,1!ir" ( + + rem Create a file to prevent SG premature post processing (ref: step (d)) .. + ECHO Copying folder "!content_path!" to "!sg_path!" > "!syncfile!" + + FOR /F "tokens=*" %%a IN ('DIR "!content_path!" /S/B') DO ( + + IF "1" == "!testmode!" ( + + ECHO XCOPY "%%a" "!sg_path!\.%%~pa" /Y/I/S + + ) ELSE ( + + XCOPY "%%a" "!sg_path!\.%%~pa" /Y/I/S >NUL 2>NUL + IF EXIST "!syncfile!" DEL "!syncfile!" + + ) + + ) + + ) ELSE ( + + rem Create a file to prevent SG premature post processing (ref: step (d)) .. + ECHO Copying file "!content_path!" to "!sg_path!" > "!syncfile!" + + IF "1" == "!testmode!" ( + + ECHO COPY "!content_path!" "!sg_path!\" /Y + + ) ELSE ( + + COPY "!content_path!" "!sg_path!\" /Y >NUL 2>NUL + IF EXIST "!syncfile!" DEL "!syncfile!" + + ) + + ) + + ) + +) +GOTO :exit + +rem **************** +rem Helper functions +rem **************** +:StartsWith text string -- Test if text starts with string +SETLOCAL +SET "txt=%~1" +SET "str=%~2" +IF DEFINED str CALL SET "s=%str%%%txt:*%str%=%%" +IF /I "%txt%" NEQ "%s%" SET=2>NUL +EXIT /B +rem **************** +rem **************** + +:exit +IF "1" == "!testmode!" PAUSE +IF "1" NEQ "!testmode!" EXIT diff --git a/autoProcessTV/onTxComplete.sample.cfg b/autoProcessTV/onTxComplete.sample.cfg new file mode 100644 index 00000000..89fee2fe --- /dev/null +++ b/autoProcessTV/onTxComplete.sample.cfg @@ -0,0 +1,26 @@ +; ============================================================================= +; ===================== EDIT THE FOLLOWING SETTINGS TO FIT ==================== +; ============================================================================= +; ====== The following settings are ONLY used by Deluge or Transmission ======= +; ============================================================================= +; File: onTxComplete.cfg + +; Path where to copy completed downloads for SG to post process. +; This value *must* .. +; 1) be different to the path where the client saves completed downloads +; 2) be configured in SG (see section "To set up SickGear" in onTxComplete.bat) +; This path should be created on first run, if not, create it yourself. + +param1=F:\sg_pp + + +; The Deluge label, or path tail used in Transmission (e.g. F:\Files\SG) +; This value should match that of the Label option set in SG at .. +; config/search/Torrent Results -> "Set torrent label/category" +; +; This script only copies files where the label begins with this value. +; However, if SG is not using labels (e.g. blackhole), then use auto label in +; the download client or a torrent completed save path ending with this value. + +param2=SG + diff --git a/gui/slick/css/browser.css b/gui/slick/css/browser.css index 50c10a74..f1275115 100644 --- a/gui/slick/css/browser.css +++ b/gui/slick/css/browser.css @@ -43,15 +43,17 @@ height:180px } -.ui-menu .ui-menu-item{ +.ui-dialog .ui-menu .ui-menu-item{ + font-size:12px; + color:#232323; background-color:#eee } -.ui-menu .ui-menu-item-alternate{ +.ui-dialog .ui-menu .ui-menu-item-alternate{ background-color:#fff } -.ui-menu a.ui-state-hover{ +.ui-dialog .ui-menu .ui-menu-item .ui-state-active{ background:none; background-color:#0A246A; color:#fff diff --git a/gui/slick/css/dark.css b/gui/slick/css/dark.css index c0113931..0f7d7eed 100644 --- a/gui/slick/css/dark.css +++ b/gui/slick/css/dark.css @@ -1,21 +1,25 @@ /* ======================================================================= inc_top.tmpl ========================================================================== */ +.shows-not-found.n .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.n, pre .prelight{ color:#c3ed9b } +.shows-not-found.nn .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.nn, pre .prelight2{ color:#f6ff41 } +.shows-not-found.nnn .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.nnn, pre .prelight-num{ color:#ffba57 } +.shows-not-found.nnnn .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.nnnn{ color:#ff6d5e } @@ -62,6 +66,10 @@ pre .prelight-num{ color:#ddd } +.ui-widget.ui-widget-content{ + border-color:#222 +} + .ui-widget-content a{ color:#2d8fbf } @@ -77,6 +85,8 @@ pre .prelight-num{ } .ui-state-default, +.ui-widget.ui-button, +.ui-widget.ui-button:active, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default{ background:#3d3d3d; @@ -84,6 +94,10 @@ pre .prelight-num{ border:1px solid #111 } +.ui-widget.ui-button:hover{ + border-color:#111 +} + .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, @@ -125,15 +139,19 @@ pre .prelight-num{ } .ui-state-hover .ui-icon, -.ui-state-focus .ui-icon{ +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon{ background-image:url("../css/lib/images/ui-icons_222222_256x240.png") } -.ui-state-active .ui-icon{ +.ui-state-active .ui-icon, +.ui-button:active .ui-icon{ background-image:url("../css/lib/images/ui-icons_8c291d_256x240.png") } -.ui-state-highlight .ui-icon{ +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon{ background-image:url("../css/lib/images/ui-icons_2e83ff_256x240.png") } @@ -142,6 +160,10 @@ pre .prelight-num{ background-image:url("../css/lib/images/ui-icons_cd0a0a_256x240.png") } +.ui-button .ui-icon{ + background-image:url("../css/lib/images/ui-icons_09a2ff_256x240.png") +} + .ui-widget-overlay{ background:#000 url("../css/lib/images/ui-bg_flat_0_000000_40x100.png") 50% 50% repeat-x } @@ -275,11 +297,13 @@ a.ui-font{ background-image:linear-gradient(to left, rgba(51, 51, 51, 1), rgba(51, 51, 51, 0)) } +.show-toggle-hide, td.tvShow a{ color:#ddd; text-decoration:none } +.show-toggle-hide:hover, td.tvShow a:hover span, td.tvShow a:hover{ cursor:pointer; @@ -315,6 +339,7 @@ home_newShow.tmpl #addRootDirTable td label .filepath, .grey-text{color:#999} .highlight-text{color:#fff} +#display-show.back-art.pro.ii .tablesorter tr .grey-text{color:#555} #newShowPortal #displayText .show-name, #newShowPortal #displayText .show-dest, @@ -619,7 +644,7 @@ config*.tmpl color:#ddd } -.testNotification{ +.test-notification{ border:1px dotted #ccc } @@ -628,6 +653,10 @@ config*.tmpl color:#ddd } +.provider-enabled{ + box-shadow:-26px 0 0 0 rgb(21, 82, 143) inset +} + #service_order_list li{ background:#333 !important; } @@ -906,6 +935,7 @@ fieldset[disabled] .navbar-default .btn-link:focus{ color:#ddd } +.component-group.typelist .bgcol, .dropdown-menu{ background-color:#333; border:1px solid rgba(0, 0, 0, 0.15); @@ -1264,16 +1294,20 @@ input sizing (for config pages) ========================================================================== */ .showlist-select optgroup, +#results-sortby optgroup, #pickShow optgroup, #showfilter optgroup, +#showsort optgroup, #editAProvider optgroup{ color:#eee; background-color:rgb(51, 51, 51) } .showlist-select optgroup option, +#results-sortby optgroup option, #pickShow optgroup option, #showfilter optgroup option, +#showsort optgroup option, #editAProvider optgroup option{ color:#222; background-color:#ddd @@ -1450,65 +1484,18 @@ thead.tablesorter-stickyHeader{ color:#000 } - /* ======================================================================= -token-input.css +token-input.css Overrides ========================================================================== */ -ul.token-input-list{ - border:1px solid #ccc; - background-color:#ddd -} - -ul.token-input-list li input{ - border:0; - background-color:white -} - -li.token-input-token{ - background-color:#d0efa0; - color:#000 -} - -li.token-input-token span{ - color:#777 -} - -li.token-input-selected-token{ - background-color:#08844e; - color:#ddd -} - -li.token-input-selected-token span{ - color:#bbb -} - -div.token-input-dropdown{ - background-color:#ddd; - color:#000; - border-left-color:#ccc; - border-right-color:#ccc; - border-bottom-color:#ccc -} - -div.token-input-dropdown p{ - color:#777 -} - +ul.token-input-list, +div.token-input-dropdown, div.token-input-dropdown ul li{ background-color:#ddd } -div.token-input-dropdown ul li.token-input-dropdown-item{ - background-color:#fafafa -} - -div.token-input-dropdown ul li.token-input-dropdown-item2{ - background-color:#ddd -} - -div.token-input-dropdown ul li.token-input-selected-dropdown-item{ - background-color:#6196c2 +li.token-input-selected-token{ + color:#ddd } /* ======================================================================= diff --git a/gui/slick/css/fonts/glyphicons-halflings-regular.eot b/gui/slick/css/fonts/glyphicons-halflings-regular.eot index 4a4ca865..b93a4953 100644 Binary files a/gui/slick/css/fonts/glyphicons-halflings-regular.eot and b/gui/slick/css/fonts/glyphicons-halflings-regular.eot differ diff --git a/gui/slick/css/fonts/glyphicons-halflings-regular.svg b/gui/slick/css/fonts/glyphicons-halflings-regular.svg index 25691af8..94fb5490 100644 --- a/gui/slick/css/fonts/glyphicons-halflings-regular.svg +++ b/gui/slick/css/fonts/glyphicons-halflings-regular.svg @@ -6,224 +6,283 @@ - - - + + - - + + - - - - - - - - - + + + + + + + + + + - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/gui/slick/css/fonts/glyphicons-halflings-regular.ttf b/gui/slick/css/fonts/glyphicons-halflings-regular.ttf index 67fa00bf..1413fc60 100644 Binary files a/gui/slick/css/fonts/glyphicons-halflings-regular.ttf and b/gui/slick/css/fonts/glyphicons-halflings-regular.ttf differ diff --git a/gui/slick/css/fonts/glyphicons-halflings-regular.woff b/gui/slick/css/fonts/glyphicons-halflings-regular.woff index 8c54182a..9e612858 100644 Binary files a/gui/slick/css/fonts/glyphicons-halflings-regular.woff and b/gui/slick/css/fonts/glyphicons-halflings-regular.woff differ diff --git a/gui/slick/css/fonts/glyphicons-halflings-regular.woff2 b/gui/slick/css/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 00000000..64539b54 Binary files /dev/null and b/gui/slick/css/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/gui/slick/css/lib/bootstrap-theme.min.css b/gui/slick/css/lib/bootstrap-theme.min.css new file mode 100644 index 00000000..5e394019 --- /dev/null +++ b/gui/slick/css/lib/bootstrap-theme.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} +/*# sourceMappingURL=bootstrap-theme.min.css.map */ \ No newline at end of file diff --git a/gui/slick/css/lib/bootstrap.css b/gui/slick/css/lib/bootstrap.css deleted file mode 100644 index 354d92db..00000000 --- a/gui/slick/css/lib/bootstrap.css +++ /dev/null @@ -1,6199 +0,0 @@ -/*! - * bootstrap v3.2.0 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! normalize.css v3.0.1 | MIT License | git.io/normalize */ -html { - font-family: sans-serif; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} -body { - margin: 0; -} -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -nav, -section, -summary { - display: block; -} -audio, -canvas, -progress, -video { - display: inline-block; - vertical-align: baseline; -} -audio:not([controls]) { - display: none; - height: 0; -} -[hidden], -template { - display: none; -} -a { - background: transparent; -} -a:active, -a:hover { - outline: 0; -} -abbr[title] { - border-bottom: 1px dotted; -} -b, -strong { - font-weight: bold; -} -dfn { - font-style: italic; -} -h1 { - margin: .67em 0; - font-size: 2em; -} -mark { - color: #000; - background: #ff0; -} -small { - font-size: 80%; -} -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} -sup { - top: -.5em; -} -sub { - bottom: -.25em; -} -img { - border: 0; -} -svg:not(:root) { - overflow: hidden; -} -figure { - margin: 1em 40px; -} -hr { - height: 0; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} -pre { - overflow: auto; -} -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} -button, -input, -optgroup, -select, -textarea { - margin: 0; - font: inherit; - color: inherit; -} -button { - overflow: visible; -} -button, -select { - text-transform: none; -} -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; - cursor: pointer; -} -button[disabled], -html input[disabled] { - cursor: default; -} -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} -input { - line-height: normal; -} -input[type="checkbox"], -input[type="radio"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 0; -} -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; -} -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} -fieldset { - padding: .35em .625em .75em; - margin: 0 2px; - border: 1px solid #c0c0c0; -} -legend { - padding: 0; - border: 0; -} -textarea { - overflow: auto; -} -optgroup { - font-weight: bold; -} -table { - border-spacing: 0; - border-collapse: collapse; -} -td, -th { - padding: 0; -} -@media print { - * { - color: #000 !important; - text-shadow: none !important; - background: transparent !important; - -webkit-box-shadow: none !important; - box-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } - select { - background: #fff !important; - } - .navbar { - display: none; - } - .table td, - .table th { - background-color: #fff !important; - } - .btn > .caret, - .dropup > .btn > .caret { - border-top-color: #000 !important; - } - .label { - border: 1px solid #000; - } - .table { - border-collapse: collapse !important; - } - .table-bordered th, - .table-bordered td { - border: 1px solid #ddd !important; - } -} -@font-face { - font-family: 'Glyphicons Halflings'; - - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\2a"; -} -.glyphicon-plus:before { - content: "\2b"; -} -.glyphicon-euro:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; -} -.glyphicon-glass:before { - content: "\e001"; -} -.glyphicon-music:before { - content: "\e002"; -} -.glyphicon-search:before { - content: "\e003"; -} -.glyphicon-heart:before { - content: "\e005"; -} -.glyphicon-star:before { - content: "\e006"; -} -.glyphicon-star-empty:before { - content: "\e007"; -} -.glyphicon-user:before { - content: "\e008"; -} -.glyphicon-film:before { - content: "\e009"; -} -.glyphicon-th-large:before { - content: "\e010"; -} -.glyphicon-th:before { - content: "\e011"; -} -.glyphicon-th-list:before { - content: "\e012"; -} -.glyphicon-ok:before { - content: "\e013"; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-zoom-in:before { - content: "\e015"; -} -.glyphicon-zoom-out:before { - content: "\e016"; -} -.glyphicon-off:before { - content: "\e017"; -} -.glyphicon-signal:before { - content: "\e018"; -} -.glyphicon-cog:before { - content: "\e019"; -} -.glyphicon-trash:before { - content: "\e020"; -} -.glyphicon-home:before { - content: "\e021"; -} -.glyphicon-file:before { - content: "\e022"; -} -.glyphicon-time:before { - content: "\e023"; -} -.glyphicon-road:before { - content: "\e024"; -} -.glyphicon-download-alt:before { - content: "\e025"; -} -.glyphicon-download:before { - content: "\e026"; -} -.glyphicon-upload:before { - content: "\e027"; -} -.glyphicon-inbox:before { - content: "\e028"; -} -.glyphicon-play-circle:before { - content: "\e029"; -} -.glyphicon-repeat:before { - content: "\e030"; -} -.glyphicon-refresh:before { - content: "\e031"; -} -.glyphicon-list-alt:before { - content: "\e032"; -} -.glyphicon-lock:before { - content: "\e033"; -} -.glyphicon-flag:before { - content: "\e034"; -} -.glyphicon-headphones:before { - content: "\e035"; -} -.glyphicon-volume-off:before { - content: "\e036"; -} -.glyphicon-volume-down:before { - content: "\e037"; -} -.glyphicon-volume-up:before { - content: "\e038"; -} -.glyphicon-qrcode:before { - content: "\e039"; -} -.glyphicon-barcode:before { - content: "\e040"; -} -.glyphicon-tag:before { - content: "\e041"; -} -.glyphicon-tags:before { - content: "\e042"; -} -.glyphicon-book:before { - content: "\e043"; -} -.glyphicon-bookmark:before { - content: "\e044"; -} -.glyphicon-print:before { - content: "\e045"; -} -.glyphicon-camera:before { - content: "\e046"; -} -.glyphicon-font:before { - content: "\e047"; -} -.glyphicon-bold:before { - content: "\e048"; -} -.glyphicon-italic:before { - content: "\e049"; -} -.glyphicon-text-height:before { - content: "\e050"; -} -.glyphicon-text-width:before { - content: "\e051"; -} -.glyphicon-align-left:before { - content: "\e052"; -} -.glyphicon-align-center:before { - content: "\e053"; -} -.glyphicon-align-right:before { - content: "\e054"; -} -.glyphicon-align-justify:before { - content: "\e055"; -} -.glyphicon-list:before { - content: "\e056"; -} -.glyphicon-indent-left:before { - content: "\e057"; -} -.glyphicon-indent-right:before { - content: "\e058"; -} -.glyphicon-facetime-video:before { - content: "\e059"; -} -.glyphicon-picture:before { - content: "\e060"; -} -.glyphicon-map-marker:before { - content: "\e062"; -} -.glyphicon-adjust:before { - content: "\e063"; -} -.glyphicon-tint:before { - content: "\e064"; -} -.glyphicon-edit:before { - content: "\e065"; -} -.glyphicon-share:before { - content: "\e066"; -} -.glyphicon-check:before { - content: "\e067"; -} -.glyphicon-move:before { - content: "\e068"; -} -.glyphicon-step-backward:before { - content: "\e069"; -} -.glyphicon-fast-backward:before { - content: "\e070"; -} -.glyphicon-backward:before { - content: "\e071"; -} -.glyphicon-play:before { - content: "\e072"; -} -.glyphicon-pause:before { - content: "\e073"; -} -.glyphicon-stop:before { - content: "\e074"; -} -.glyphicon-forward:before { - content: "\e075"; -} -.glyphicon-fast-forward:before { - content: "\e076"; -} -.glyphicon-step-forward:before { - content: "\e077"; -} -.glyphicon-eject:before { - content: "\e078"; -} -.glyphicon-chevron-left:before { - content: "\e079"; -} -.glyphicon-chevron-right:before { - content: "\e080"; -} -.glyphicon-plus-sign:before { - content: "\e081"; -} -.glyphicon-minus-sign:before { - content: "\e082"; -} -.glyphicon-remove-sign:before { - content: "\e083"; -} -.glyphicon-ok-sign:before { - content: "\e084"; -} -.glyphicon-question-sign:before { - content: "\e085"; -} -.glyphicon-info-sign:before { - content: "\e086"; -} -.glyphicon-screenshot:before { - content: "\e087"; -} -.glyphicon-remove-circle:before { - content: "\e088"; -} -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; -} -.glyphicon-arrow-left:before { - content: "\e091"; -} -.glyphicon-arrow-right:before { - content: "\e092"; -} -.glyphicon-arrow-up:before { - content: "\e093"; -} -.glyphicon-arrow-down:before { - content: "\e094"; -} -.glyphicon-share-alt:before { - content: "\e095"; -} -.glyphicon-resize-full:before { - content: "\e096"; -} -.glyphicon-resize-small:before { - content: "\e097"; -} -.glyphicon-exclamation-sign:before { - content: "\e101"; -} -.glyphicon-gift:before { - content: "\e102"; -} -.glyphicon-leaf:before { - content: "\e103"; -} -.glyphicon-fire:before { - content: "\e104"; -} -.glyphicon-eye-open:before { - content: "\e105"; -} -.glyphicon-eye-close:before { - content: "\e106"; -} -.glyphicon-warning-sign:before { - content: "\e107"; -} -.glyphicon-plane:before { - content: "\e108"; -} -.glyphicon-calendar:before { - content: "\e109"; -} -.glyphicon-random:before { - content: "\e110"; -} -.glyphicon-comment:before { - content: "\e111"; -} -.glyphicon-magnet:before { - content: "\e112"; -} -.glyphicon-chevron-up:before { - content: "\e113"; -} -.glyphicon-chevron-down:before { - content: "\e114"; -} -.glyphicon-retweet:before { - content: "\e115"; -} -.glyphicon-shopping-cart:before { - content: "\e116"; -} -.glyphicon-folder-close:before { - content: "\e117"; -} -.glyphicon-folder-open:before { - content: "\e118"; -} -.glyphicon-resize-vertical:before { - content: "\e119"; -} -.glyphicon-resize-horizontal:before { - content: "\e120"; -} -.glyphicon-hdd:before { - content: "\e121"; -} -.glyphicon-bullhorn:before { - content: "\e122"; -} -.glyphicon-bell:before { - content: "\e123"; -} -.glyphicon-certificate:before { - content: "\e124"; -} -.glyphicon-thumbs-up:before { - content: "\e125"; -} -.glyphicon-thumbs-down:before { - content: "\e126"; -} -.glyphicon-hand-right:before { - content: "\e127"; -} -.glyphicon-hand-left:before { - content: "\e128"; -} -.glyphicon-hand-up:before { - content: "\e129"; -} -.glyphicon-hand-down:before { - content: "\e130"; -} -.glyphicon-circle-arrow-right:before { - content: "\e131"; -} -.glyphicon-circle-arrow-left:before { - content: "\e132"; -} -.glyphicon-circle-arrow-up:before { - content: "\e133"; -} -.glyphicon-circle-arrow-down:before { - content: "\e134"; -} -.glyphicon-globe:before { - content: "\e135"; -} -.glyphicon-wrench:before { - content: "\e136"; -} -.glyphicon-tasks:before { - content: "\e137"; -} -.glyphicon-filter:before { - content: "\e138"; -} -.glyphicon-briefcase:before { - content: "\e139"; -} -.glyphicon-fullscreen:before { - content: "\e140"; -} -.glyphicon-dashboard:before { - content: "\e141"; -} -.glyphicon-paperclip:before { - content: "\e142"; -} -.glyphicon-heart-empty:before { - content: "\e143"; -} -.glyphicon-link:before { - content: "\e144"; -} -.glyphicon-phone:before { - content: "\e145"; -} -.glyphicon-pushpin:before { - content: "\e146"; -} -.glyphicon-usd:before { - content: "\e148"; -} -.glyphicon-gbp:before { - content: "\e149"; -} -.glyphicon-sort:before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet:before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; -} -.glyphicon-sort-by-order:before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt:before { - content: "\e154"; -} -.glyphicon-sort-by-attributes:before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; -} -.glyphicon-unchecked:before { - content: "\e157"; -} -.glyphicon-expand:before { - content: "\e158"; -} -.glyphicon-collapse-down:before { - content: "\e159"; -} -.glyphicon-collapse-up:before { - content: "\e160"; -} -.glyphicon-log-in:before { - content: "\e161"; -} -.glyphicon-flash:before { - content: "\e162"; -} -.glyphicon-log-out:before { - content: "\e163"; -} -.glyphicon-new-window:before { - content: "\e164"; -} -.glyphicon-record:before { - content: "\e165"; -} -.glyphicon-save:before { - content: "\e166"; -} -.glyphicon-open:before { - content: "\e167"; -} -.glyphicon-saved:before { - content: "\e168"; -} -.glyphicon-import:before { - content: "\e169"; -} -.glyphicon-export:before { - content: "\e170"; -} -.glyphicon-send:before { - content: "\e171"; -} -.glyphicon-floppy-disk:before { - content: "\e172"; -} -.glyphicon-floppy-saved:before { - content: "\e173"; -} -.glyphicon-floppy-remove:before { - content: "\e174"; -} -.glyphicon-floppy-save:before { - content: "\e175"; -} -.glyphicon-floppy-open:before { - content: "\e176"; -} -.glyphicon-credit-card:before { - content: "\e177"; -} -.glyphicon-transfer:before { - content: "\e178"; -} -.glyphicon-cutlery:before { - content: "\e179"; -} -.glyphicon-header:before { - content: "\e180"; -} -.glyphicon-compressed:before { - content: "\e181"; -} -.glyphicon-earphone:before { - content: "\e182"; -} -.glyphicon-phone-alt:before { - content: "\e183"; -} -.glyphicon-tower:before { - content: "\e184"; -} -.glyphicon-stats:before { - content: "\e185"; -} -.glyphicon-sd-video:before { - content: "\e186"; -} -.glyphicon-hd-video:before { - content: "\e187"; -} -.glyphicon-subtitles:before { - content: "\e188"; -} -.glyphicon-sound-stereo:before { - content: "\e189"; -} -.glyphicon-sound-dolby:before { - content: "\e190"; -} -.glyphicon-sound-5-1:before { - content: "\e191"; -} -.glyphicon-sound-6-1:before { - content: "\e192"; -} -.glyphicon-sound-7-1:before { - content: "\e193"; -} -.glyphicon-copyright-mark:before { - content: "\e194"; -} -.glyphicon-registration-mark:before { - content: "\e195"; -} -.glyphicon-cloud-download:before { - content: "\e197"; -} -.glyphicon-cloud-upload:before { - content: "\e198"; -} -.glyphicon-tree-conifer:before { - content: "\e199"; -} -.glyphicon-tree-deciduous:before { - content: "\e200"; -} -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -*:before, -*:after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -html { - font-size: 10px; - - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} -body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.42857143; - color: #333; - background-color: #fff; -} -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} -a { - color: #428bca; - text-decoration: none; -} -a:hover, -a:focus { - color: #2a6496; - text-decoration: underline; -} -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -figure { - margin: 0; -} -img { - vertical-align: middle; -} -.img-responsive, -.thumbnail > img, -.thumbnail a > img, -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - width: 100% \9; - max-width: 100%; - height: auto; -} -.img-rounded { - border-radius: 6px; -} -.img-thumbnail { - display: inline-block; - width: 100% \9; - max-width: 100%; - height: auto; - padding: 4px; - line-height: 1.42857143; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: all .2s ease-in-out; - -o-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; -} -.img-circle { - border-radius: 50%; -} -hr { - margin-top: 20px; - margin-bottom: 20px; - border: 0; - border-top: 1px solid #eee; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} -h1, -h2, -h3, -h4, -h5, -h6, -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-family: inherit; - font-weight: 500; - line-height: 1.1; - color: inherit; -} -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small, -.h1 small, -.h2 small, -.h3 small, -.h4 small, -.h5 small, -.h6 small, -h1 .small, -h2 .small, -h3 .small, -h4 .small, -h5 .small, -h6 .small, -.h1 .small, -.h2 .small, -.h3 .small, -.h4 .small, -.h5 .small, -.h6 .small { - font-weight: normal; - line-height: 1; - color: #777; -} -h1, -.h1, -h2, -.h2, -h3, -.h3 { - margin-top: 20px; - margin-bottom: 10px; -} -h1 small, -.h1 small, -h2 small, -.h2 small, -h3 small, -.h3 small, -h1 .small, -.h1 .small, -h2 .small, -.h2 .small, -h3 .small, -.h3 .small { - font-size: 65%; -} -h4, -.h4, -h5, -.h5, -h6, -.h6 { - margin-top: 10px; - margin-bottom: 10px; -} -h4 small, -.h4 small, -h5 small, -.h5 small, -h6 small, -.h6 small, -h4 .small, -.h4 .small, -h5 .small, -.h5 .small, -h6 .small, -.h6 .small { - font-size: 75%; -} -h1, -.h1 { - font-size: 36px; -} -h2, -.h2 { - font-size: 30px; -} -h3, -.h3 { - font-size: 24px; -} -h4, -.h4 { - font-size: 18px; -} -h5, -.h5 { - font-size: 14px; -} -h6, -.h6 { - font-size: 12px; -} -p { - margin: 0 0 10px; -} -.lead { - margin-bottom: 20px; - font-size: 16px; - font-weight: 300; - line-height: 1.4; -} -@media (min-width: 768px) { - .lead { - font-size: 21px; - } -} -small, -.small { - font-size: 85%; -} -cite { - font-style: normal; -} -mark, -.mark { - padding: .2em; - background-color: #fcf8e3; -} -.text-left { - text-align: left; -} -.text-right { - text-align: right; -} -.text-center { - text-align: center; -} -.text-justify { - text-align: justify; -} -.text-nowrap { - white-space: nowrap; -} -.text-lowercase { - text-transform: lowercase; -} -.text-uppercase { - text-transform: uppercase; -} -.text-capitalize { - text-transform: capitalize; -} -.text-muted { - color: #777; -} -.text-primary { - color: #428bca; -} -a.text-primary:hover { - color: #3071a9; -} -.text-success { - color: #3c763d; -} -a.text-success:hover { - color: #2b542c; -} -.text-info { - color: #31708f; -} -a.text-info:hover { - color: #245269; -} -.text-warning { - color: #8a6d3b; -} -a.text-warning:hover { - color: #66512c; -} -.text-danger { - color: #a94442; -} -a.text-danger:hover { - color: #843534; -} -.bg-primary { - color: #fff; - background-color: #428bca; -} -a.bg-primary:hover { - background-color: #3071a9; -} -.bg-success { - background-color: #dff0d8; -} -a.bg-success:hover { - background-color: #c1e2b3; -} -.bg-info { - background-color: #d9edf7; -} -a.bg-info:hover { - background-color: #afd9ee; -} -.bg-warning { - background-color: #fcf8e3; -} -a.bg-warning:hover { - background-color: #f7ecb5; -} -.bg-danger { - background-color: #f2dede; -} -a.bg-danger:hover { - background-color: #e4b9b9; -} -.page-header { - padding-bottom: 9px; - margin: 40px 0 20px; - border-bottom: 1px solid #eee; -} -ul, -ol { - margin-top: 0; - margin-bottom: 10px; -} -ul ul, -ol ul, -ul ol, -ol ol { - margin-bottom: 0; -} -.list-unstyled { - padding-left: 0; - list-style: none; -} -.list-inline { - padding-left: 0; - margin-left: -5px; - list-style: none; -} -.list-inline > li { - display: inline-block; - padding-right: 5px; - padding-left: 5px; -} -dl { - margin-top: 0; - margin-bottom: 20px; -} -dt, -dd { - line-height: 1.42857143; -} -dt { - font-weight: bold; -} -dd { - margin-left: 0; -} -@media (min-width: 768px) { - .dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; - } - .dl-horizontal dd { - margin-left: 180px; - } -} -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #777; -} -.initialism { - font-size: 90%; - text-transform: uppercase; -} -blockquote { - padding: 10px 20px; - margin: 0 0 20px; - font-size: 17.5px; - border-left: 5px solid #eee; -} -blockquote p:last-child, -blockquote ul:last-child, -blockquote ol:last-child { - margin-bottom: 0; -} -blockquote footer, -blockquote small, -blockquote .small { - display: block; - font-size: 80%; - line-height: 1.42857143; - color: #777; -} -blockquote footer:before, -blockquote small:before, -blockquote .small:before { - content: '\2014 \00A0'; -} -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - text-align: right; - border-right: 5px solid #eee; - border-left: 0; -} -.blockquote-reverse footer:before, -blockquote.pull-right footer:before, -.blockquote-reverse small:before, -blockquote.pull-right small:before, -.blockquote-reverse .small:before, -blockquote.pull-right .small:before { - content: ''; -} -.blockquote-reverse footer:after, -blockquote.pull-right footer:after, -.blockquote-reverse small:after, -blockquote.pull-right small:after, -.blockquote-reverse .small:after, -blockquote.pull-right .small:after { - content: '\00A0 \2014'; -} -blockquote:before, -blockquote:after { - content: ""; -} -address { - margin-bottom: 20px; - font-style: normal; - line-height: 1.42857143; -} -code, -kbd, -pre, -samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; -} -code { - padding: 2px 4px; - font-size: 90%; - color: #c7254e; - background-color: #f9f2f4; - border-radius: 4px; -} -kbd { - padding: 2px 4px; - font-size: 90%; - color: #fff; - background-color: #333; - border-radius: 3px; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); -} -kbd kbd { - padding: 0; - font-size: 100%; - -webkit-box-shadow: none; - box-shadow: none; -} -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 1.42857143; - color: #333; - word-break: break-all; - word-wrap: break-word; - background-color: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; -} -pre code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; -} -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} -.container { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} -@media (min-width: 768px) { - .container { - width: 750px; - } -} -@media (min-width: 992px) { - .container { - width: 970px; - } -} -@media (min-width: 1200px) { - .container { - width: 1170px; - } -} -.container-fluid { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} -.row { - margin-right: -15px; - margin-left: -15px; -} -.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { - position: relative; - min-height: 1px; - padding-right: 15px; - padding-left: 15px; -} -.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { - float: left; -} -.col-xs-12 { - width: 100%; -} -.col-xs-11 { - width: 91.66666667%; -} -.col-xs-10 { - width: 83.33333333%; -} -.col-xs-9 { - width: 75%; -} -.col-xs-8 { - width: 66.66666667%; -} -.col-xs-7 { - width: 58.33333333%; -} -.col-xs-6 { - width: 50%; -} -.col-xs-5 { - width: 41.66666667%; -} -.col-xs-4 { - width: 33.33333333%; -} -.col-xs-3 { - width: 25%; -} -.col-xs-2 { - width: 16.66666667%; -} -.col-xs-1 { - width: 8.33333333%; -} -.col-xs-pull-12 { - right: 100%; -} -.col-xs-pull-11 { - right: 91.66666667%; -} -.col-xs-pull-10 { - right: 83.33333333%; -} -.col-xs-pull-9 { - right: 75%; -} -.col-xs-pull-8 { - right: 66.66666667%; -} -.col-xs-pull-7 { - right: 58.33333333%; -} -.col-xs-pull-6 { - right: 50%; -} -.col-xs-pull-5 { - right: 41.66666667%; -} -.col-xs-pull-4 { - right: 33.33333333%; -} -.col-xs-pull-3 { - right: 25%; -} -.col-xs-pull-2 { - right: 16.66666667%; -} -.col-xs-pull-1 { - right: 8.33333333%; -} -.col-xs-pull-0 { - right: auto; -} -.col-xs-push-12 { - left: 100%; -} -.col-xs-push-11 { - left: 91.66666667%; -} -.col-xs-push-10 { - left: 83.33333333%; -} -.col-xs-push-9 { - left: 75%; -} -.col-xs-push-8 { - left: 66.66666667%; -} -.col-xs-push-7 { - left: 58.33333333%; -} -.col-xs-push-6 { - left: 50%; -} -.col-xs-push-5 { - left: 41.66666667%; -} -.col-xs-push-4 { - left: 33.33333333%; -} -.col-xs-push-3 { - left: 25%; -} -.col-xs-push-2 { - left: 16.66666667%; -} -.col-xs-push-1 { - left: 8.33333333%; -} -.col-xs-push-0 { - left: auto; -} -.col-xs-offset-12 { - margin-left: 100%; -} -.col-xs-offset-11 { - margin-left: 91.66666667%; -} -.col-xs-offset-10 { - margin-left: 83.33333333%; -} -.col-xs-offset-9 { - margin-left: 75%; -} -.col-xs-offset-8 { - margin-left: 66.66666667%; -} -.col-xs-offset-7 { - margin-left: 58.33333333%; -} -.col-xs-offset-6 { - margin-left: 50%; -} -.col-xs-offset-5 { - margin-left: 41.66666667%; -} -.col-xs-offset-4 { - margin-left: 33.33333333%; -} -.col-xs-offset-3 { - margin-left: 25%; -} -.col-xs-offset-2 { - margin-left: 16.66666667%; -} -.col-xs-offset-1 { - margin-left: 8.33333333%; -} -.col-xs-offset-0 { - margin-left: 0; -} -@media (min-width: 768px) { - .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { - float: left; - } - .col-sm-12 { - width: 100%; - } - .col-sm-11 { - width: 91.66666667%; - } - .col-sm-10 { - width: 83.33333333%; - } - .col-sm-9 { - width: 75%; - } - .col-sm-8 { - width: 66.66666667%; - } - .col-sm-7 { - width: 58.33333333%; - } - .col-sm-6 { - width: 50%; - } - .col-sm-5 { - width: 41.66666667%; - } - .col-sm-4 { - width: 33.33333333%; - } - .col-sm-3 { - width: 25%; - } - .col-sm-2 { - width: 16.66666667%; - } - .col-sm-1 { - width: 8.33333333%; - } - .col-sm-pull-12 { - right: 100%; - } - .col-sm-pull-11 { - right: 91.66666667%; - } - .col-sm-pull-10 { - right: 83.33333333%; - } - .col-sm-pull-9 { - right: 75%; - } - .col-sm-pull-8 { - right: 66.66666667%; - } - .col-sm-pull-7 { - right: 58.33333333%; - } - .col-sm-pull-6 { - right: 50%; - } - .col-sm-pull-5 { - right: 41.66666667%; - } - .col-sm-pull-4 { - right: 33.33333333%; - } - .col-sm-pull-3 { - right: 25%; - } - .col-sm-pull-2 { - right: 16.66666667%; - } - .col-sm-pull-1 { - right: 8.33333333%; - } - .col-sm-pull-0 { - right: auto; - } - .col-sm-push-12 { - left: 100%; - } - .col-sm-push-11 { - left: 91.66666667%; - } - .col-sm-push-10 { - left: 83.33333333%; - } - .col-sm-push-9 { - left: 75%; - } - .col-sm-push-8 { - left: 66.66666667%; - } - .col-sm-push-7 { - left: 58.33333333%; - } - .col-sm-push-6 { - left: 50%; - } - .col-sm-push-5 { - left: 41.66666667%; - } - .col-sm-push-4 { - left: 33.33333333%; - } - .col-sm-push-3 { - left: 25%; - } - .col-sm-push-2 { - left: 16.66666667%; - } - .col-sm-push-1 { - left: 8.33333333%; - } - .col-sm-push-0 { - left: auto; - } - .col-sm-offset-12 { - margin-left: 100%; - } - .col-sm-offset-11 { - margin-left: 91.66666667%; - } - .col-sm-offset-10 { - margin-left: 83.33333333%; - } - .col-sm-offset-9 { - margin-left: 75%; - } - .col-sm-offset-8 { - margin-left: 66.66666667%; - } - .col-sm-offset-7 { - margin-left: 58.33333333%; - } - .col-sm-offset-6 { - margin-left: 50%; - } - .col-sm-offset-5 { - margin-left: 41.66666667%; - } - .col-sm-offset-4 { - margin-left: 33.33333333%; - } - .col-sm-offset-3 { - margin-left: 25%; - } - .col-sm-offset-2 { - margin-left: 16.66666667%; - } - .col-sm-offset-1 { - margin-left: 8.33333333%; - } - .col-sm-offset-0 { - margin-left: 0; - } -} -@media (min-width: 992px) { - .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { - float: left; - } - .col-md-12 { - width: 100%; - } - .col-md-11 { - width: 91.66666667%; - } - .col-md-10 { - width: 83.33333333%; - } - .col-md-9 { - width: 75%; - } - .col-md-8 { - width: 66.66666667%; - } - .col-md-7 { - width: 58.33333333%; - } - .col-md-6 { - width: 50%; - } - .col-md-5 { - width: 41.66666667%; - } - .col-md-4 { - width: 33.33333333%; - } - .col-md-3 { - width: 25%; - } - .col-md-2 { - width: 16.66666667%; - } - .col-md-1 { - width: 8.33333333%; - } - .col-md-pull-12 { - right: 100%; - } - .col-md-pull-11 { - right: 91.66666667%; - } - .col-md-pull-10 { - right: 83.33333333%; - } - .col-md-pull-9 { - right: 75%; - } - .col-md-pull-8 { - right: 66.66666667%; - } - .col-md-pull-7 { - right: 58.33333333%; - } - .col-md-pull-6 { - right: 50%; - } - .col-md-pull-5 { - right: 41.66666667%; - } - .col-md-pull-4 { - right: 33.33333333%; - } - .col-md-pull-3 { - right: 25%; - } - .col-md-pull-2 { - right: 16.66666667%; - } - .col-md-pull-1 { - right: 8.33333333%; - } - .col-md-pull-0 { - right: auto; - } - .col-md-push-12 { - left: 100%; - } - .col-md-push-11 { - left: 91.66666667%; - } - .col-md-push-10 { - left: 83.33333333%; - } - .col-md-push-9 { - left: 75%; - } - .col-md-push-8 { - left: 66.66666667%; - } - .col-md-push-7 { - left: 58.33333333%; - } - .col-md-push-6 { - left: 50%; - } - .col-md-push-5 { - left: 41.66666667%; - } - .col-md-push-4 { - left: 33.33333333%; - } - .col-md-push-3 { - left: 25%; - } - .col-md-push-2 { - left: 16.66666667%; - } - .col-md-push-1 { - left: 8.33333333%; - } - .col-md-push-0 { - left: auto; - } - .col-md-offset-12 { - margin-left: 100%; - } - .col-md-offset-11 { - margin-left: 91.66666667%; - } - .col-md-offset-10 { - margin-left: 83.33333333%; - } - .col-md-offset-9 { - margin-left: 75%; - } - .col-md-offset-8 { - margin-left: 66.66666667%; - } - .col-md-offset-7 { - margin-left: 58.33333333%; - } - .col-md-offset-6 { - margin-left: 50%; - } - .col-md-offset-5 { - margin-left: 41.66666667%; - } - .col-md-offset-4 { - margin-left: 33.33333333%; - } - .col-md-offset-3 { - margin-left: 25%; - } - .col-md-offset-2 { - margin-left: 16.66666667%; - } - .col-md-offset-1 { - margin-left: 8.33333333%; - } - .col-md-offset-0 { - margin-left: 0; - } -} -@media (min-width: 1200px) { - .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { - float: left; - } - .col-lg-12 { - width: 100%; - } - .col-lg-11 { - width: 91.66666667%; - } - .col-lg-10 { - width: 83.33333333%; - } - .col-lg-9 { - width: 75%; - } - .col-lg-8 { - width: 66.66666667%; - } - .col-lg-7 { - width: 58.33333333%; - } - .col-lg-6 { - width: 50%; - } - .col-lg-5 { - width: 41.66666667%; - } - .col-lg-4 { - width: 33.33333333%; - } - .col-lg-3 { - width: 25%; - } - .col-lg-2 { - width: 16.66666667%; - } - .col-lg-1 { - width: 8.33333333%; - } - .col-lg-pull-12 { - right: 100%; - } - .col-lg-pull-11 { - right: 91.66666667%; - } - .col-lg-pull-10 { - right: 83.33333333%; - } - .col-lg-pull-9 { - right: 75%; - } - .col-lg-pull-8 { - right: 66.66666667%; - } - .col-lg-pull-7 { - right: 58.33333333%; - } - .col-lg-pull-6 { - right: 50%; - } - .col-lg-pull-5 { - right: 41.66666667%; - } - .col-lg-pull-4 { - right: 33.33333333%; - } - .col-lg-pull-3 { - right: 25%; - } - .col-lg-pull-2 { - right: 16.66666667%; - } - .col-lg-pull-1 { - right: 8.33333333%; - } - .col-lg-pull-0 { - right: auto; - } - .col-lg-push-12 { - left: 100%; - } - .col-lg-push-11 { - left: 91.66666667%; - } - .col-lg-push-10 { - left: 83.33333333%; - } - .col-lg-push-9 { - left: 75%; - } - .col-lg-push-8 { - left: 66.66666667%; - } - .col-lg-push-7 { - left: 58.33333333%; - } - .col-lg-push-6 { - left: 50%; - } - .col-lg-push-5 { - left: 41.66666667%; - } - .col-lg-push-4 { - left: 33.33333333%; - } - .col-lg-push-3 { - left: 25%; - } - .col-lg-push-2 { - left: 16.66666667%; - } - .col-lg-push-1 { - left: 8.33333333%; - } - .col-lg-push-0 { - left: auto; - } - .col-lg-offset-12 { - margin-left: 100%; - } - .col-lg-offset-11 { - margin-left: 91.66666667%; - } - .col-lg-offset-10 { - margin-left: 83.33333333%; - } - .col-lg-offset-9 { - margin-left: 75%; - } - .col-lg-offset-8 { - margin-left: 66.66666667%; - } - .col-lg-offset-7 { - margin-left: 58.33333333%; - } - .col-lg-offset-6 { - margin-left: 50%; - } - .col-lg-offset-5 { - margin-left: 41.66666667%; - } - .col-lg-offset-4 { - margin-left: 33.33333333%; - } - .col-lg-offset-3 { - margin-left: 25%; - } - .col-lg-offset-2 { - margin-left: 16.66666667%; - } - .col-lg-offset-1 { - margin-left: 8.33333333%; - } - .col-lg-offset-0 { - margin-left: 0; - } -} -table { - background-color: transparent; -} -th { - text-align: left; -} -.table { - width: 100%; - max-width: 100%; - margin-bottom: 20px; -} -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - padding: 8px; - line-height: 1.42857143; - vertical-align: top; - border-top: 1px solid #ddd; -} -.table > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid #ddd; -} -.table > caption + thead > tr:first-child > th, -.table > colgroup + thead > tr:first-child > th, -.table > thead:first-child > tr:first-child > th, -.table > caption + thead > tr:first-child > td, -.table > colgroup + thead > tr:first-child > td, -.table > thead:first-child > tr:first-child > td { - border-top: 0; -} -.table > tbody + tbody { - border-top: 2px solid #ddd; -} -.table .table { - background-color: #fff; -} -.table-condensed > thead > tr > th, -.table-condensed > tbody > tr > th, -.table-condensed > tfoot > tr > th, -.table-condensed > thead > tr > td, -.table-condensed > tbody > tr > td, -.table-condensed > tfoot > tr > td { - padding: 5px; -} -.table-bordered { - border: 1px solid #ddd; -} -.table-bordered > thead > tr > th, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > tbody > tr > td, -.table-bordered > tfoot > tr > td { - border: 1px solid #ddd; -} -.table-bordered > thead > tr > th, -.table-bordered > thead > tr > td { - border-bottom-width: 2px; -} -.table-striped > tbody > tr:nth-child(odd) > td, -.table-striped > tbody > tr:nth-child(odd) > th { - background-color: #f9f9f9; -} -.table-hover > tbody > tr:hover > td, -.table-hover > tbody > tr:hover > th { - background-color: #f5f5f5; -} -table col[class*="col-"] { - position: static; - display: table-column; - float: none; -} -table td[class*="col-"], -table th[class*="col-"] { - position: static; - display: table-cell; - float: none; -} -.table > thead > tr > td.active, -.table > tbody > tr > td.active, -.table > tfoot > tr > td.active, -.table > thead > tr > th.active, -.table > tbody > tr > th.active, -.table > tfoot > tr > th.active, -.table > thead > tr.active > td, -.table > tbody > tr.active > td, -.table > tfoot > tr.active > td, -.table > thead > tr.active > th, -.table > tbody > tr.active > th, -.table > tfoot > tr.active > th { - background-color: #f5f5f5; -} -.table-hover > tbody > tr > td.active:hover, -.table-hover > tbody > tr > th.active:hover, -.table-hover > tbody > tr.active:hover > td, -.table-hover > tbody > tr:hover > .active, -.table-hover > tbody > tr.active:hover > th { - background-color: #e8e8e8; -} -.table > thead > tr > td.success, -.table > tbody > tr > td.success, -.table > tfoot > tr > td.success, -.table > thead > tr > th.success, -.table > tbody > tr > th.success, -.table > tfoot > tr > th.success, -.table > thead > tr.success > td, -.table > tbody > tr.success > td, -.table > tfoot > tr.success > td, -.table > thead > tr.success > th, -.table > tbody > tr.success > th, -.table > tfoot > tr.success > th { - background-color: #dff0d8; -} -.table-hover > tbody > tr > td.success:hover, -.table-hover > tbody > tr > th.success:hover, -.table-hover > tbody > tr.success:hover > td, -.table-hover > tbody > tr:hover > .success, -.table-hover > tbody > tr.success:hover > th { - background-color: #d0e9c6; -} -.table > thead > tr > td.info, -.table > tbody > tr > td.info, -.table > tfoot > tr > td.info, -.table > thead > tr > th.info, -.table > tbody > tr > th.info, -.table > tfoot > tr > th.info, -.table > thead > tr.info > td, -.table > tbody > tr.info > td, -.table > tfoot > tr.info > td, -.table > thead > tr.info > th, -.table > tbody > tr.info > th, -.table > tfoot > tr.info > th { - background-color: #d9edf7; -} -.table-hover > tbody > tr > td.info:hover, -.table-hover > tbody > tr > th.info:hover, -.table-hover > tbody > tr.info:hover > td, -.table-hover > tbody > tr:hover > .info, -.table-hover > tbody > tr.info:hover > th { - background-color: #c4e3f3; -} -.table > thead > tr > td.warning, -.table > tbody > tr > td.warning, -.table > tfoot > tr > td.warning, -.table > thead > tr > th.warning, -.table > tbody > tr > th.warning, -.table > tfoot > tr > th.warning, -.table > thead > tr.warning > td, -.table > tbody > tr.warning > td, -.table > tfoot > tr.warning > td, -.table > thead > tr.warning > th, -.table > tbody > tr.warning > th, -.table > tfoot > tr.warning > th { - background-color: #fcf8e3; -} -.table-hover > tbody > tr > td.warning:hover, -.table-hover > tbody > tr > th.warning:hover, -.table-hover > tbody > tr.warning:hover > td, -.table-hover > tbody > tr:hover > .warning, -.table-hover > tbody > tr.warning:hover > th { - background-color: #faf2cc; -} -.table > thead > tr > td.danger, -.table > tbody > tr > td.danger, -.table > tfoot > tr > td.danger, -.table > thead > tr > th.danger, -.table > tbody > tr > th.danger, -.table > tfoot > tr > th.danger, -.table > thead > tr.danger > td, -.table > tbody > tr.danger > td, -.table > tfoot > tr.danger > td, -.table > thead > tr.danger > th, -.table > tbody > tr.danger > th, -.table > tfoot > tr.danger > th { - background-color: #f2dede; -} -.table-hover > tbody > tr > td.danger:hover, -.table-hover > tbody > tr > th.danger:hover, -.table-hover > tbody > tr.danger:hover > td, -.table-hover > tbody > tr:hover > .danger, -.table-hover > tbody > tr.danger:hover > th { - background-color: #ebcccc; -} -@media screen and (max-width: 767px) { - .table-responsive { - width: 100%; - margin-bottom: 15px; - overflow-x: auto; - overflow-y: hidden; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #ddd; - } - .table-responsive > .table { - margin-bottom: 0; - } - .table-responsive > .table > thead > tr > th, - .table-responsive > .table > tbody > tr > th, - .table-responsive > .table > tfoot > tr > th, - .table-responsive > .table > thead > tr > td, - .table-responsive > .table > tbody > tr > td, - .table-responsive > .table > tfoot > tr > td { - white-space: nowrap; - } - .table-responsive > .table-bordered { - border: 0; - } - .table-responsive > .table-bordered > thead > tr > th:first-child, - .table-responsive > .table-bordered > tbody > tr > th:first-child, - .table-responsive > .table-bordered > tfoot > tr > th:first-child, - .table-responsive > .table-bordered > thead > tr > td:first-child, - .table-responsive > .table-bordered > tbody > tr > td:first-child, - .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; - } - .table-responsive > .table-bordered > thead > tr > th:last-child, - .table-responsive > .table-bordered > tbody > tr > th:last-child, - .table-responsive > .table-bordered > tfoot > tr > th:last-child, - .table-responsive > .table-bordered > thead > tr > td:last-child, - .table-responsive > .table-bordered > tbody > tr > td:last-child, - .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; - } - .table-responsive > .table-bordered > tbody > tr:last-child > th, - .table-responsive > .table-bordered > tfoot > tr:last-child > th, - .table-responsive > .table-bordered > tbody > tr:last-child > td, - .table-responsive > .table-bordered > tfoot > tr:last-child > td { - border-bottom: 0; - } -} -fieldset { - min-width: 0; - padding: 0; - margin: 0; - border: 0; -} -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: inherit; - color: #333; - border: 0; - border-bottom: 1px solid #e5e5e5; -} -label { - display: inline-block; - max-width: 100%; - margin-bottom: 5px; - font-weight: bold; -} -input[type="search"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - line-height: normal; -} -input[type="file"] { - display: block; -} -input[type="range"] { - display: block; - width: 100%; -} -select[multiple], -select[size] { - height: auto; -} -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -output { - display: block; - padding-top: 7px; - font-size: 14px; - line-height: 1.42857143; - color: #555; -} -.form-control { - display: block; - width: 100%; - height: 34px; - padding: 6px 12px; - font-size: 14px; - line-height: 1.42857143; - color: #555; - background-color: #fff; - background-image: none; - border: 1px solid #ccc; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -} -.form-control:focus { - border-color: #66afe9; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); -} -.form-control::-moz-placeholder { - color: #777; - opacity: 1; -} -.form-control:-ms-input-placeholder { - color: #777; -} -.form-control::-webkit-input-placeholder { - color: #777; -} -.form-control[disabled], -.form-control[readonly], -fieldset[disabled] .form-control { - cursor: not-allowed; - background-color: #eee; - opacity: 1; -} -textarea.form-control { - height: auto; -} -input[type="search"] { - -webkit-appearance: none; -} -input[type="date"], -input[type="time"], -input[type="datetime-local"], -input[type="month"] { - line-height: 34px; - line-height: 1.42857143 \0; -} -input[type="date"].input-sm, -input[type="time"].input-sm, -input[type="datetime-local"].input-sm, -input[type="month"].input-sm { - line-height: 30px; -} -input[type="date"].input-lg, -input[type="time"].input-lg, -input[type="datetime-local"].input-lg, -input[type="month"].input-lg { - line-height: 46px; -} -.form-group { - margin-bottom: 15px; -} -.radio, -.checkbox { - position: relative; - display: block; - min-height: 20px; - margin-top: 10px; - margin-bottom: 10px; -} -.radio label, -.checkbox label { - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-top: 4px \9; - margin-left: -20px; -} -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; -} -.radio-inline, -.checkbox-inline { - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - vertical-align: middle; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; -} -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"].disabled, -input[type="checkbox"].disabled, -fieldset[disabled] input[type="radio"], -fieldset[disabled] input[type="checkbox"] { - cursor: not-allowed; -} -.radio-inline.disabled, -.checkbox-inline.disabled, -fieldset[disabled] .radio-inline, -fieldset[disabled] .checkbox-inline { - cursor: not-allowed; -} -.radio.disabled label, -.checkbox.disabled label, -fieldset[disabled] .radio label, -fieldset[disabled] .checkbox label { - cursor: not-allowed; -} -.form-control-static { - padding-top: 7px; - padding-bottom: 7px; - margin-bottom: 0; -} -.form-control-static.input-lg, -.form-control-static.input-sm { - padding-right: 0; - padding-left: 0; -} -.input-sm, -.form-horizontal .form-group-sm .form-control { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-sm { - height: 30px; - line-height: 30px; -} -textarea.input-sm, -select[multiple].input-sm { - height: auto; -} -.input-lg, -.form-horizontal .form-group-lg .form-control { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.33; - border-radius: 6px; -} -select.input-lg { - height: 46px; - line-height: 46px; -} -textarea.input-lg, -select[multiple].input-lg { - height: auto; -} -.has-feedback { - position: relative; -} -.has-feedback .form-control { - padding-right: 42.5px; -} -.form-control-feedback { - position: absolute; - top: 25px; - right: 0; - z-index: 2; - display: block; - width: 34px; - height: 34px; - line-height: 34px; - text-align: center; -} -.input-lg + .form-control-feedback { - width: 46px; - height: 46px; - line-height: 46px; -} -.input-sm + .form-control-feedback { - width: 30px; - height: 30px; - line-height: 30px; -} -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline { - color: #3c763d; -} -.has-success .form-control { - border-color: #3c763d; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-success .form-control:focus { - border-color: #2b542c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; -} -.has-success .input-group-addon { - color: #3c763d; - background-color: #dff0d8; - border-color: #3c763d; -} -.has-success .form-control-feedback { - color: #3c763d; -} -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline { - color: #8a6d3b; -} -.has-warning .form-control { - border-color: #8a6d3b; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-warning .form-control:focus { - border-color: #66512c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; -} -.has-warning .input-group-addon { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #8a6d3b; -} -.has-warning .form-control-feedback { - color: #8a6d3b; -} -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline { - color: #a94442; -} -.has-error .form-control { - border-color: #a94442; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-error .form-control:focus { - border-color: #843534; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; -} -.has-error .input-group-addon { - color: #a94442; - background-color: #f2dede; - border-color: #a94442; -} -.has-error .form-control-feedback { - color: #a94442; -} -.has-feedback label.sr-only ~ .form-control-feedback { - top: 0; -} -.help-block { - display: block; - margin-top: 5px; - margin-bottom: 10px; - color: #737373; -} -@media (min-width: 768px) { - .form-inline .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .form-inline .input-group { - display: inline-table; - vertical-align: middle; - } - .form-inline .input-group .input-group-addon, - .form-inline .input-group .input-group-btn, - .form-inline .input-group .form-control { - width: auto; - } - .form-inline .input-group > .form-control { - width: 100%; - } - .form-inline .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio, - .form-inline .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio label, - .form-inline .checkbox label { - padding-left: 0; - } - .form-inline .radio input[type="radio"], - .form-inline .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .form-inline .has-feedback .form-control-feedback { - top: 0; - } -} -.form-horizontal .radio, -.form-horizontal .checkbox, -.form-horizontal .radio-inline, -.form-horizontal .checkbox-inline { - padding-top: 7px; - margin-top: 0; - margin-bottom: 0; -} -.form-horizontal .radio, -.form-horizontal .checkbox { - min-height: 27px; -} -.form-horizontal .form-group { - margin-right: -15px; - margin-left: -15px; -} -@media (min-width: 768px) { - .form-horizontal .control-label { - padding-top: 7px; - margin-bottom: 0; - text-align: right; - } -} -.form-horizontal .has-feedback .form-control-feedback { - top: 0; - right: 15px; -} -@media (min-width: 768px) { - .form-horizontal .form-group-lg .control-label { - padding-top: 14.3px; - } -} -@media (min-width: 768px) { - .form-horizontal .form-group-sm .control-label { - padding-top: 6px; - } -} -.btn { - display: inline-block; - padding: 6px 12px; - margin-bottom: 0; - font-size: 14px; - font-weight: normal; - line-height: 1.42857143; - text-align: center; - white-space: nowrap; - vertical-align: middle; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.btn:focus, -.btn:active:focus, -.btn.active:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.btn:hover, -.btn:focus { - color: #333; - text-decoration: none; -} -.btn:active, -.btn.active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn.disabled, -.btn[disabled], -fieldset[disabled] .btn { - pointer-events: none; - cursor: not-allowed; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; - opacity: .65; -} -.btn-default { - color: #333; - background-color: #fff; - border-color: #ccc; -} -.btn-default:hover, -.btn-default:focus, -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - color: #333; - background-color: #e6e6e6; - border-color: #adadad; -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - background-image: none; -} -.btn-default.disabled, -.btn-default[disabled], -fieldset[disabled] .btn-default, -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled:active, -.btn-default[disabled]:active, -fieldset[disabled] .btn-default:active, -.btn-default.disabled.active, -.btn-default[disabled].active, -fieldset[disabled] .btn-default.active { - background-color: #fff; - border-color: #ccc; -} -.btn-default .badge { - color: #fff; - background-color: #333; -} -.btn-primary { - color: #fff; - background-color: #428bca; - border-color: #357ebd; -} -.btn-primary:hover, -.btn-primary:focus, -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - color: #fff; - background-color: #3071a9; - border-color: #285e8e; -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - background-image: none; -} -.btn-primary.disabled, -.btn-primary[disabled], -fieldset[disabled] .btn-primary, -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled:active, -.btn-primary[disabled]:active, -fieldset[disabled] .btn-primary:active, -.btn-primary.disabled.active, -.btn-primary[disabled].active, -fieldset[disabled] .btn-primary.active { - background-color: #428bca; - border-color: #357ebd; -} -.btn-primary .badge { - color: #428bca; - background-color: #fff; -} -.btn-success { - color: #fff; - background-color: #5cb85c; - border-color: #4cae4c; -} -.btn-success:hover, -.btn-success:focus, -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - color: #fff; - background-color: #449d44; - border-color: #398439; -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - background-image: none; -} -.btn-success.disabled, -.btn-success[disabled], -fieldset[disabled] .btn-success, -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled:active, -.btn-success[disabled]:active, -fieldset[disabled] .btn-success:active, -.btn-success.disabled.active, -.btn-success[disabled].active, -fieldset[disabled] .btn-success.active { - background-color: #5cb85c; - border-color: #4cae4c; -} -.btn-success .badge { - color: #5cb85c; - background-color: #fff; -} -.btn-info { - color: #fff; - background-color: #5bc0de; - border-color: #46b8da; -} -.btn-info:hover, -.btn-info:focus, -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - color: #fff; - background-color: #31b0d5; - border-color: #269abc; -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - background-image: none; -} -.btn-info.disabled, -.btn-info[disabled], -fieldset[disabled] .btn-info, -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled:active, -.btn-info[disabled]:active, -fieldset[disabled] .btn-info:active, -.btn-info.disabled.active, -.btn-info[disabled].active, -fieldset[disabled] .btn-info.active { - background-color: #5bc0de; - border-color: #46b8da; -} -.btn-info .badge { - color: #5bc0de; - background-color: #fff; -} -.btn-warning { - color: #fff; - background-color: #f0ad4e; - border-color: #eea236; -} -.btn-warning:hover, -.btn-warning:focus, -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - color: #fff; - background-color: #ec971f; - border-color: #d58512; -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - background-image: none; -} -.btn-warning.disabled, -.btn-warning[disabled], -fieldset[disabled] .btn-warning, -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled:active, -.btn-warning[disabled]:active, -fieldset[disabled] .btn-warning:active, -.btn-warning.disabled.active, -.btn-warning[disabled].active, -fieldset[disabled] .btn-warning.active { - background-color: #f0ad4e; - border-color: #eea236; -} -.btn-warning .badge { - color: #f0ad4e; - background-color: #fff; -} -.btn-danger { - color: #fff; - background-color: #d9534f; - border-color: #d43f3a; -} -.btn-danger:hover, -.btn-danger:focus, -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - color: #fff; - background-color: #c9302c; - border-color: #ac2925; -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - background-image: none; -} -.btn-danger.disabled, -.btn-danger[disabled], -fieldset[disabled] .btn-danger, -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled:active, -.btn-danger[disabled]:active, -fieldset[disabled] .btn-danger:active, -.btn-danger.disabled.active, -.btn-danger[disabled].active, -fieldset[disabled] .btn-danger.active { - background-color: #d9534f; - border-color: #d43f3a; -} -.btn-danger .badge { - color: #d9534f; - background-color: #fff; -} -.btn-link { - font-weight: normal; - color: #428bca; - cursor: pointer; - border-radius: 0; -} -.btn-link, -.btn-link:active, -.btn-link[disabled], -fieldset[disabled] .btn-link { - background-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; -} -.btn-link, -.btn-link:hover, -.btn-link:focus, -.btn-link:active { - border-color: transparent; -} -.btn-link:hover, -.btn-link:focus { - color: #2a6496; - text-decoration: underline; - background-color: transparent; -} -.btn-link[disabled]:hover, -fieldset[disabled] .btn-link:hover, -.btn-link[disabled]:focus, -fieldset[disabled] .btn-link:focus { - color: #777; - text-decoration: none; -} -.btn-lg, -.btn-group-lg > .btn { - padding: 10px 16px; - font-size: 18px; - line-height: 1.33; - border-radius: 6px; -} -.btn-sm, -.btn-group-sm > .btn { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-xs, -.btn-group-xs > .btn { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-block { - display: block; - width: 100%; -} -.btn-block + .btn-block { - margin-top: 5px; -} -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} -.fade { - opacity: 0; - -webkit-transition: opacity .15s linear; - -o-transition: opacity .15s linear; - transition: opacity .15s linear; -} -.fade.in { - opacity: 1; -} -.collapse { - display: none; -} -.collapse.in { - display: block; -} -tr.collapse.in { - display: table-row; -} -tbody.collapse.in { - display: table-row-group; -} -.collapsing { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition: height .35s ease; - -o-transition: height .35s ease; - transition: height .35s ease; -} -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 4px solid; - border-right: 4px solid transparent; - border-left: 4px solid transparent; -} -.dropdown { - position: relative; -} -.dropdown-toggle:focus { - outline: 0; -} -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - font-size: 14px; - text-align: left; - list-style: none; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .15); - border-radius: 4px; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); - box-shadow: 0 6px 12px rgba(0, 0, 0, .175); -} -.dropdown-menu.pull-right { - right: 0; - left: auto; -} -.dropdown-menu .divider { - height: 1px; - margin: 9px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.42857143; - color: #333; - white-space: nowrap; -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - color: #262626; - text-decoration: none; - background-color: #f5f5f5; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #fff; - text-decoration: none; - background-color: #428bca; - outline: 0; -} -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #777; -} -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - cursor: not-allowed; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.open > .dropdown-menu { - display: block; -} -.open > a { - outline: 0; -} -.dropdown-menu-right { - right: 0; - left: auto; -} -.dropdown-menu-left { - right: auto; - left: 0; -} -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: 12px; - line-height: 1.42857143; - color: #777; - white-space: nowrap; -} -.dropdown-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 990; -} -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - content: ""; - border-top: 0; - border-bottom: 4px solid; -} -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 1px; -} -@media (min-width: 768px) { - .navbar-right .dropdown-menu { - right: 0; - left: auto; - } - .navbar-right .dropdown-menu-left { - right: auto; - left: 0; - } -} -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; -} -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - float: left; -} -.btn-group > .btn:hover, -.btn-group-vertical > .btn:hover, -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus, -.btn-group > .btn:active, -.btn-group-vertical > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn.active { - z-index: 2; -} -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus { - outline: 0; -} -.btn-group .btn + .btn, -.btn-group .btn + .btn-group, -.btn-group .btn-group + .btn, -.btn-group .btn-group + .btn-group { - margin-left: -1px; -} -.btn-toolbar { - margin-left: -5px; -} -.btn-toolbar .btn-group, -.btn-toolbar .input-group { - float: left; -} -.btn-toolbar > .btn, -.btn-toolbar > .btn-group, -.btn-toolbar > .input-group { - margin-left: 5px; -} -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} -.btn-group > .btn:first-child { - margin-left: 0; -} -.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child > .btn:last-child, -.btn-group > .btn-group:first-child > .dropdown-toggle { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group > .btn-group:last-child > .btn:first-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} -.btn-group > .btn + .dropdown-toggle { - padding-right: 8px; - padding-left: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-right: 12px; - padding-left: 12px; -} -.btn-group.open .dropdown-toggle { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn-group.open .dropdown-toggle.btn-link { - -webkit-box-shadow: none; - box-shadow: none; -} -.btn .caret { - margin-left: 0; -} -.btn-lg .caret { - border-width: 5px 5px 0; - border-bottom-width: 0; -} -.dropup .btn-lg .caret { - border-width: 0 5px 5px; -} -.btn-group-vertical > .btn, -.btn-group-vertical > .btn-group, -.btn-group-vertical > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; -} -.btn-group-vertical > .btn-group > .btn { - float: none; -} -.btn-group-vertical > .btn + .btn, -.btn-group-vertical > .btn + .btn-group, -.btn-group-vertical > .btn-group + .btn, -.btn-group-vertical > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; -} -.btn-group-vertical > .btn:not(:first-child):not(:last-child) { - border-radius: 0; -} -.btn-group-vertical > .btn:first-child:not(:last-child) { - border-top-right-radius: 4px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 4px; -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; -} -.btn-group-justified > .btn, -.btn-group-justified > .btn-group { - display: table-cell; - float: none; - width: 1%; -} -.btn-group-justified > .btn-group .btn { - width: 100%; -} -.btn-group-justified > .btn-group .dropdown-menu { - left: auto; -} -[data-toggle="buttons"] > .btn > input[type="radio"], -[data-toggle="buttons"] > .btn > input[type="checkbox"] { - position: absolute; - z-index: -1; - filter: alpha(opacity=0); - opacity: 0; -} -.input-group { - position: relative; - display: table; - border-collapse: separate; -} -.input-group[class*="col-"] { - float: none; - padding-right: 0; - padding-left: 0; -} -.input-group .form-control { - position: relative; - z-index: 2; - float: left; - width: 100%; - margin-bottom: 0; -} -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.33; - border-radius: 6px; -} -select.input-group-lg > .form-control, -select.input-group-lg > .input-group-addon, -select.input-group-lg > .input-group-btn > .btn { - height: 46px; - line-height: 46px; -} -textarea.input-group-lg > .form-control, -textarea.input-group-lg > .input-group-addon, -textarea.input-group-lg > .input-group-btn > .btn, -select[multiple].input-group-lg > .form-control, -select[multiple].input-group-lg > .input-group-addon, -select[multiple].input-group-lg > .input-group-btn > .btn { - height: auto; -} -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-group-sm > .form-control, -select.input-group-sm > .input-group-addon, -select.input-group-sm > .input-group-btn > .btn { - height: 30px; - line-height: 30px; -} -textarea.input-group-sm > .form-control, -textarea.input-group-sm > .input-group-addon, -textarea.input-group-sm > .input-group-btn > .btn, -select[multiple].input-group-sm > .form-control, -select[multiple].input-group-sm > .input-group-addon, -select[multiple].input-group-sm > .input-group-btn > .btn { - height: auto; -} -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; -} -.input-group-addon:not(:first-child):not(:last-child), -.input-group-btn:not(:first-child):not(:last-child), -.input-group .form-control:not(:first-child):not(:last-child) { - border-radius: 0; -} -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; -} -.input-group-addon { - padding: 6px 12px; - font-size: 14px; - font-weight: normal; - line-height: 1; - color: #555; - text-align: center; - background-color: #eee; - border: 1px solid #ccc; - border-radius: 4px; -} -.input-group-addon.input-sm { - padding: 5px 10px; - font-size: 12px; - border-radius: 3px; -} -.input-group-addon.input-lg { - padding: 10px 16px; - font-size: 18px; - border-radius: 6px; -} -.input-group-addon input[type="radio"], -.input-group-addon input[type="checkbox"] { - margin-top: 0; -} -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.input-group-addon:last-child { - border-left: 0; -} -.input-group-btn { - position: relative; - font-size: 0; - white-space: nowrap; -} -.input-group-btn > .btn { - position: relative; -} -.input-group-btn > .btn + .btn { - margin-left: -1px; -} -.input-group-btn > .btn:hover, -.input-group-btn > .btn:focus, -.input-group-btn > .btn:active { - z-index: 2; -} -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group { - margin-right: -1px; -} -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group { - margin-left: -1px; -} -.nav { - padding-left: 0; - margin-bottom: 0; - list-style: none; -} -.nav > li { - position: relative; - display: block; -} -.nav > li > a { - position: relative; - display: block; - padding: 10px 15px; -} -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eee; -} -.nav > li.disabled > a { - color: #777; -} -.nav > li.disabled > a:hover, -.nav > li.disabled > a:focus { - color: #777; - text-decoration: none; - cursor: not-allowed; - background-color: transparent; -} -.nav .open > a, -.nav .open > a:hover, -.nav .open > a:focus { - background-color: #eee; - border-color: #428bca; -} -.nav .nav-divider { - height: 1px; - margin: 9px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.nav > li > a > img { - max-width: none; -} -.nav-tabs { - border-bottom: 1px solid #ddd; -} -.nav-tabs > li { - float: left; - margin-bottom: -1px; -} -.nav-tabs > li > a { - margin-right: 2px; - line-height: 1.42857143; - border: 1px solid transparent; - border-radius: 4px 4px 0 0; -} -.nav-tabs > li > a:hover { - border-color: #eee #eee #ddd; -} -.nav-tabs > li.active > a, -.nav-tabs > li.active > a:hover, -.nav-tabs > li.active > a:focus { - color: #555; - cursor: default; - background-color: #fff; - border: 1px solid #ddd; - border-bottom-color: transparent; -} -.nav-tabs.nav-justified { - width: 100%; - border-bottom: 0; -} -.nav-tabs.nav-justified > li { - float: none; -} -.nav-tabs.nav-justified > li > a { - margin-bottom: 5px; - text-align: center; -} -.nav-tabs.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-tabs.nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs.nav-justified > li > a { - margin-right: 0; - border-radius: 4px; -} -.nav-tabs.nav-justified > .active > a, -.nav-tabs.nav-justified > .active > a:hover, -.nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #ddd; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; - } - .nav-tabs.nav-justified > .active > a, - .nav-tabs.nav-justified > .active > a:hover, - .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #fff; - } -} -.nav-pills > li { - float: left; -} -.nav-pills > li > a { - border-radius: 4px; -} -.nav-pills > li + li { - margin-left: 2px; -} -.nav-pills > li.active > a, -.nav-pills > li.active > a:hover, -.nav-pills > li.active > a:focus { - color: #fff; - background-color: #428bca; -} -.nav-stacked > li { - float: none; -} -.nav-stacked > li + li { - margin-top: 2px; - margin-left: 0; -} -.nav-justified { - width: 100%; -} -.nav-justified > li { - float: none; -} -.nav-justified > li > a { - margin-bottom: 5px; - text-align: center; -} -.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs-justified { - border-bottom: 0; -} -.nav-tabs-justified > li > a { - margin-right: 0; - border-radius: 4px; -} -.nav-tabs-justified > .active > a, -.nav-tabs-justified > .active > a:hover, -.nav-tabs-justified > .active > a:focus { - border: 1px solid #ddd; -} -@media (min-width: 768px) { - .nav-tabs-justified > li > a { - border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; - } - .nav-tabs-justified > .active > a, - .nav-tabs-justified > .active > a:hover, - .nav-tabs-justified > .active > a:focus { - border-bottom-color: #fff; - } -} -.tab-content > .tab-pane { - display: none; -} -.tab-content > .active { - display: block; -} -.nav-tabs .dropdown-menu { - margin-top: -1px; - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.navbar { - position: relative; - min-height: 50px; - margin-bottom: 20px; - border: 1px solid transparent; -} -@media (min-width: 768px) { - .navbar { - border-radius: 4px; - } -} -@media (min-width: 768px) { - .navbar-header { - float: left; - } -} -.navbar-collapse { - padding-right: 15px; - padding-left: 15px; - overflow-x: visible; - -webkit-overflow-scrolling: touch; - border-top: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); -} -.navbar-collapse.in { - overflow-y: auto; -} -@media (min-width: 768px) { - .navbar-collapse { - width: auto; - border-top: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-collapse.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; - overflow: visible !important; - } - .navbar-collapse.in { - overflow-y: visible; - } - .navbar-fixed-top .navbar-collapse, - .navbar-static-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - padding-right: 0; - padding-left: 0; - } -} -.navbar-fixed-top .navbar-collapse, -.navbar-fixed-bottom .navbar-collapse { - max-height: 340px; -} -@media (max-width: 480px) and (orientation: landscape) { - .navbar-fixed-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - max-height: 200px; - } -} -.container > .navbar-header, -.container-fluid > .navbar-header, -.container > .navbar-collapse, -.container-fluid > .navbar-collapse { - margin-right: -15px; - margin-left: -15px; -} -@media (min-width: 768px) { - .container > .navbar-header, - .container-fluid > .navbar-header, - .container > .navbar-collapse, - .container-fluid > .navbar-collapse { - margin-right: 0; - margin-left: 0; - } -} -.navbar-static-top { - z-index: 1000; - border-width: 0 0 1px; -} -@media (min-width: 768px) { - .navbar-static-top { - border-radius: 0; - } -} -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; -} -@media (min-width: 768px) { - .navbar-fixed-top, - .navbar-fixed-bottom { - border-radius: 0; - } -} -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; -} -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; - border-width: 1px 0 0; -} -.navbar-brand { - float: left; - height: 50px; - padding: 15px 15px; - font-size: 18px; - line-height: 20px; -} -.navbar-brand:hover, -.navbar-brand:focus { - text-decoration: none; -} -@media (min-width: 768px) { - .navbar > .container .navbar-brand, - .navbar > .container-fluid .navbar-brand { - margin-left: -15px; - } -} -.navbar-toggle { - position: relative; - float: right; - padding: 9px 10px; - margin-top: 8px; - margin-right: 15px; - margin-bottom: 8px; - background-color: transparent; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.navbar-toggle:focus { - outline: 0; -} -.navbar-toggle .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; -} -.navbar-toggle .icon-bar + .icon-bar { - margin-top: 4px; -} -@media (min-width: 768px) { - .navbar-toggle { - display: none; - } -} -.navbar-nav { - margin: 7.5px -15px; -} -.navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: 20px; -} -@media (max-width: 767px) { - .navbar-nav .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-nav .open .dropdown-menu > li > a, - .navbar-nav .open .dropdown-menu .dropdown-header { - padding: 5px 15px 5px 25px; - } - .navbar-nav .open .dropdown-menu > li > a { - line-height: 20px; - } - .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-nav .open .dropdown-menu > li > a:focus { - background-image: none; - } -} -@media (min-width: 768px) { - .navbar-nav { - float: left; - margin: 0; - } - .navbar-nav > li { - float: left; - } - .navbar-nav > li > a { - padding-top: 15px; - padding-bottom: 15px; - } - .navbar-nav.navbar-right:last-child { - margin-right: -15px; - } -} -@media (min-width: 768px) { - .navbar-left { - float: left !important; - } - .navbar-right { - float: right !important; - } -} -.navbar-form { - padding: 10px 15px; - margin-top: 8px; - margin-right: -15px; - margin-bottom: 8px; - margin-left: -15px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); -} -@media (min-width: 768px) { - .navbar-form .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .navbar-form .input-group { - display: inline-table; - vertical-align: middle; - } - .navbar-form .input-group .input-group-addon, - .navbar-form .input-group .input-group-btn, - .navbar-form .input-group .form-control { - width: auto; - } - .navbar-form .input-group > .form-control { - width: 100%; - } - .navbar-form .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio, - .navbar-form .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio label, - .navbar-form .checkbox label { - padding-left: 0; - } - .navbar-form .radio input[type="radio"], - .navbar-form .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .navbar-form .has-feedback .form-control-feedback { - top: 0; - } -} -@media (max-width: 767px) { - .navbar-form .form-group { - margin-bottom: 5px; - } -} -@media (min-width: 768px) { - .navbar-form { - width: auto; - padding-top: 0; - padding-bottom: 0; - margin-right: 0; - margin-left: 0; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-form.navbar-right:last-child { - margin-right: -15px; - } -} -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.navbar-btn { - margin-top: 8px; - margin-bottom: 8px; -} -.navbar-btn.btn-sm { - margin-top: 10px; - margin-bottom: 10px; -} -.navbar-btn.btn-xs { - margin-top: 14px; - margin-bottom: 14px; -} -.navbar-text { - margin-top: 15px; - margin-bottom: 15px; -} -@media (min-width: 768px) { - .navbar-text { - float: left; - margin-right: 15px; - margin-left: 15px; - } - .navbar-text.navbar-right:last-child { - margin-right: 0; - } -} -.navbar-default { - background-color: #f8f8f8; - border-color: #e7e7e7; -} -.navbar-default .navbar-brand { - color: #777; -} -.navbar-default .navbar-brand:hover, -.navbar-default .navbar-brand:focus { - color: #5e5e5e; - background-color: transparent; -} -.navbar-default .navbar-text { - color: #777; -} -.navbar-default .navbar-nav > li > a { - color: #777; -} -.navbar-default .navbar-nav > li > a:hover, -.navbar-default .navbar-nav > li > a:focus { - color: #333; - background-color: transparent; -} -.navbar-default .navbar-nav > .active > a, -.navbar-default .navbar-nav > .active > a:hover, -.navbar-default .navbar-nav > .active > a:focus { - color: #555; - background-color: #e7e7e7; -} -.navbar-default .navbar-nav > .disabled > a, -.navbar-default .navbar-nav > .disabled > a:hover, -.navbar-default .navbar-nav > .disabled > a:focus { - color: #ccc; - background-color: transparent; -} -.navbar-default .navbar-toggle { - border-color: #ddd; -} -.navbar-default .navbar-toggle:hover, -.navbar-default .navbar-toggle:focus { - background-color: #ddd; -} -.navbar-default .navbar-toggle .icon-bar { - background-color: #888; -} -.navbar-default .navbar-collapse, -.navbar-default .navbar-form { - border-color: #e7e7e7; -} -.navbar-default .navbar-nav > .open > a, -.navbar-default .navbar-nav > .open > a:hover, -.navbar-default .navbar-nav > .open > a:focus { - color: #555; - background-color: #e7e7e7; -} -@media (max-width: 767px) { - .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #777; - } - .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #333; - background-color: transparent; - } - .navbar-default .navbar-nav .open .dropdown-menu > .active > a, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #555; - background-color: #e7e7e7; - } - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #ccc; - background-color: transparent; - } -} -.navbar-default .navbar-link { - color: #777; -} -.navbar-default .navbar-link:hover { - color: #333; -} -.navbar-default .btn-link { - color: #777; -} -.navbar-default .btn-link:hover, -.navbar-default .btn-link:focus { - color: #333; -} -.navbar-default .btn-link[disabled]:hover, -fieldset[disabled] .navbar-default .btn-link:hover, -.navbar-default .btn-link[disabled]:focus, -fieldset[disabled] .navbar-default .btn-link:focus { - color: #ccc; -} -.navbar-inverse { - background-color: #222; - border-color: #080808; -} -.navbar-inverse .navbar-brand { - color: #777; -} -.navbar-inverse .navbar-brand:hover, -.navbar-inverse .navbar-brand:focus { - color: #fff; - background-color: transparent; -} -.navbar-inverse .navbar-text { - color: #777; -} -.navbar-inverse .navbar-nav > li > a { - color: #777; -} -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > li > a:focus { - color: #fff; - background-color: transparent; -} -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:hover, -.navbar-inverse .navbar-nav > .active > a:focus { - color: #fff; - background-color: #080808; -} -.navbar-inverse .navbar-nav > .disabled > a, -.navbar-inverse .navbar-nav > .disabled > a:hover, -.navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444; - background-color: transparent; -} -.navbar-inverse .navbar-toggle { - border-color: #333; -} -.navbar-inverse .navbar-toggle:hover, -.navbar-inverse .navbar-toggle:focus { - background-color: #333; -} -.navbar-inverse .navbar-toggle .icon-bar { - background-color: #fff; -} -.navbar-inverse .navbar-collapse, -.navbar-inverse .navbar-form { - border-color: #101010; -} -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .open > a:hover, -.navbar-inverse .navbar-nav > .open > a:focus { - color: #fff; - background-color: #080808; -} -@media (max-width: 767px) { - .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { - border-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #777; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #fff; - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #fff; - background-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444; - background-color: transparent; - } -} -.navbar-inverse .navbar-link { - color: #777; -} -.navbar-inverse .navbar-link:hover { - color: #fff; -} -.navbar-inverse .btn-link { - color: #777; -} -.navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link:focus { - color: #fff; -} -.navbar-inverse .btn-link[disabled]:hover, -fieldset[disabled] .navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link[disabled]:focus, -fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444; -} -.breadcrumb { - padding: 8px 15px; - margin-bottom: 20px; - list-style: none; - background-color: #f5f5f5; - border-radius: 4px; -} -.breadcrumb > li { - display: inline-block; -} -.breadcrumb > li + li:before { - padding: 0 5px; - color: #ccc; - content: "/\00a0"; -} -.breadcrumb > .active { - color: #777; -} -.pagination { - display: inline-block; - padding-left: 0; - margin: 20px 0; - border-radius: 4px; -} -.pagination > li { - display: inline; -} -.pagination > li > a, -.pagination > li > span { - position: relative; - float: left; - padding: 6px 12px; - margin-left: -1px; - line-height: 1.42857143; - color: #428bca; - text-decoration: none; - background-color: #fff; - border: 1px solid #ddd; -} -.pagination > li:first-child > a, -.pagination > li:first-child > span { - margin-left: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} -.pagination > li:last-child > a, -.pagination > li:last-child > span { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} -.pagination > li > a:hover, -.pagination > li > span:hover, -.pagination > li > a:focus, -.pagination > li > span:focus { - color: #2a6496; - background-color: #eee; - border-color: #ddd; -} -.pagination > .active > a, -.pagination > .active > span, -.pagination > .active > a:hover, -.pagination > .active > span:hover, -.pagination > .active > a:focus, -.pagination > .active > span:focus { - z-index: 2; - color: #fff; - cursor: default; - background-color: #428bca; - border-color: #428bca; -} -.pagination > .disabled > span, -.pagination > .disabled > span:hover, -.pagination > .disabled > span:focus, -.pagination > .disabled > a, -.pagination > .disabled > a:hover, -.pagination > .disabled > a:focus { - color: #777; - cursor: not-allowed; - background-color: #fff; - border-color: #ddd; -} -.pagination-lg > li > a, -.pagination-lg > li > span { - padding: 10px 16px; - font-size: 18px; -} -.pagination-lg > li:first-child > a, -.pagination-lg > li:first-child > span { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; -} -.pagination-lg > li:last-child > a, -.pagination-lg > li:last-child > span { - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; -} -.pagination-sm > li > a, -.pagination-sm > li > span { - padding: 5px 10px; - font-size: 12px; -} -.pagination-sm > li:first-child > a, -.pagination-sm > li:first-child > span { - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; -} -.pagination-sm > li:last-child > a, -.pagination-sm > li:last-child > span { - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} -.pager { - padding-left: 0; - margin: 20px 0; - text-align: center; - list-style: none; -} -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 15px; -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #eee; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; -} -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #777; - cursor: not-allowed; - background-color: #fff; -} -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; -} -a.label:hover, -a.label:focus { - color: #fff; - text-decoration: none; - cursor: pointer; -} -.label:empty { - display: none; -} -.btn .label { - position: relative; - top: -1px; -} -.label-default { - background-color: #777; -} -.label-default[href]:hover, -.label-default[href]:focus { - background-color: #5e5e5e; -} -.label-primary { - background-color: #428bca; -} -.label-primary[href]:hover, -.label-primary[href]:focus { - background-color: #3071a9; -} -.label-success { - background-color: #5cb85c; -} -.label-success[href]:hover, -.label-success[href]:focus { - background-color: #449d44; -} -.label-info { - background-color: #5bc0de; -} -.label-info[href]:hover, -.label-info[href]:focus { - background-color: #31b0d5; -} -.label-warning { - background-color: #f0ad4e; -} -.label-warning[href]:hover, -.label-warning[href]:focus { - background-color: #ec971f; -} -.label-danger { - background-color: #d9534f; -} -.label-danger[href]:hover, -.label-danger[href]:focus { - background-color: #c9302c; -} -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - background-color: #777; - border-radius: 10px; -} -.badge:empty { - display: none; -} -.btn .badge { - position: relative; - top: -1px; -} -.btn-xs .badge { - top: 0; - padding: 1px 5px; -} -a.badge:hover, -a.badge:focus { - color: #fff; - text-decoration: none; - cursor: pointer; -} -a.list-group-item.active > .badge, -.nav-pills > .active > a > .badge { - color: #428bca; - background-color: #fff; -} -.nav-pills > li > a > .badge { - margin-left: 3px; -} -.jumbotron { - padding: 30px; - margin-bottom: 30px; - color: inherit; - background-color: #eee; -} -.jumbotron h1, -.jumbotron .h1 { - color: inherit; -} -.jumbotron p { - margin-bottom: 15px; - font-size: 21px; - font-weight: 200; -} -.jumbotron > hr { - border-top-color: #d5d5d5; -} -.container .jumbotron { - border-radius: 6px; -} -.jumbotron .container { - max-width: 100%; -} -@media screen and (min-width: 768px) { - .jumbotron { - padding-top: 48px; - padding-bottom: 48px; - } - .container .jumbotron { - padding-right: 60px; - padding-left: 60px; - } - .jumbotron h1, - .jumbotron .h1 { - font-size: 63px; - } -} -.thumbnail { - display: block; - padding: 4px; - margin-bottom: 20px; - line-height: 1.42857143; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: all .2s ease-in-out; - -o-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; -} -.thumbnail > img, -.thumbnail a > img { - margin-right: auto; - margin-left: auto; -} -a.thumbnail:hover, -a.thumbnail:focus, -a.thumbnail.active { - border-color: #428bca; -} -.thumbnail .caption { - padding: 9px; - color: #333; -} -.alert { - padding: 15px; - margin-bottom: 20px; - border: 1px solid transparent; - border-radius: 4px; -} -.alert h4 { - margin-top: 0; - color: inherit; -} -.alert .alert-link { - font-weight: bold; -} -.alert > p, -.alert > ul { - margin-bottom: 0; -} -.alert > p + p { - margin-top: 5px; -} -.alert-dismissable, -.alert-dismissible { - padding-right: 35px; -} -.alert-dismissable .close, -.alert-dismissible .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; -} -.alert-success { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; -} -.alert-success hr { - border-top-color: #c9e2b3; -} -.alert-success .alert-link { - color: #2b542c; -} -.alert-info { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; -} -.alert-info hr { - border-top-color: #a6e1ec; -} -.alert-info .alert-link { - color: #245269; -} -.alert-warning { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; -} -.alert-warning hr { - border-top-color: #f7e1b5; -} -.alert-warning .alert-link { - color: #66512c; -} -.alert-danger { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; -} -.alert-danger hr { - border-top-color: #e4b9c0; -} -.alert-danger .alert-link { - color: #843534; -} -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@-o-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -.progress { - height: 20px; - margin-bottom: 20px; - overflow: hidden; - background-color: #f5f5f5; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); -} -.progress-bar { - float: left; - width: 0; - height: 100%; - font-size: 12px; - line-height: 20px; - color: #fff; - text-align: center; - background-color: #428bca; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - -webkit-transition: width .6s ease; - -o-transition: width .6s ease; - transition: width .6s ease; -} -.progress-striped .progress-bar, -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - background-size: 40px 40px; -} -.progress.active .progress-bar, -.progress-bar.active { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} -.progress-bar[aria-valuenow="1"], -.progress-bar[aria-valuenow="2"] { - min-width: 30px; -} -.progress-bar[aria-valuenow="0"] { - min-width: 30px; - color: #777; - background-color: transparent; - background-image: none; - -webkit-box-shadow: none; - box-shadow: none; -} -.progress-bar-success { - background-color: #5cb85c; -} -.progress-striped .progress-bar-success { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-info { - background-color: #5bc0de; -} -.progress-striped .progress-bar-info { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-warning { - background-color: #f0ad4e; -} -.progress-striped .progress-bar-warning { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-danger { - background-color: #d9534f; -} -.progress-striped .progress-bar-danger { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.media, -.media-body { - overflow: hidden; - zoom: 1; -} -.media, -.media .media { - margin-top: 15px; -} -.media:first-child { - margin-top: 0; -} -.media-object { - display: block; -} -.media-heading { - margin: 0 0 5px; -} -.media > .pull-left { - margin-right: 10px; -} -.media > .pull-right { - margin-left: 10px; -} -.media-list { - padding-left: 0; - list-style: none; -} -.list-group { - padding-left: 0; - margin-bottom: 20px; -} -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #fff; - border: 1px solid #ddd; -} -.list-group-item:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; -} -.list-group-item:last-child { - margin-bottom: 0; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; -} -.list-group-item > .badge { - float: right; -} -.list-group-item > .badge + .badge { - margin-right: 5px; -} -a.list-group-item { - color: #555; -} -a.list-group-item .list-group-item-heading { - color: #333; -} -a.list-group-item:hover, -a.list-group-item:focus { - color: #555; - text-decoration: none; - background-color: #f5f5f5; -} -.list-group-item.disabled, -.list-group-item.disabled:hover, -.list-group-item.disabled:focus { - color: #777; - background-color: #eee; -} -.list-group-item.disabled .list-group-item-heading, -.list-group-item.disabled:hover .list-group-item-heading, -.list-group-item.disabled:focus .list-group-item-heading { - color: inherit; -} -.list-group-item.disabled .list-group-item-text, -.list-group-item.disabled:hover .list-group-item-text, -.list-group-item.disabled:focus .list-group-item-text { - color: #777; -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - z-index: 2; - color: #fff; - background-color: #428bca; - border-color: #428bca; -} -.list-group-item.active .list-group-item-heading, -.list-group-item.active:hover .list-group-item-heading, -.list-group-item.active:focus .list-group-item-heading, -.list-group-item.active .list-group-item-heading > small, -.list-group-item.active:hover .list-group-item-heading > small, -.list-group-item.active:focus .list-group-item-heading > small, -.list-group-item.active .list-group-item-heading > .small, -.list-group-item.active:hover .list-group-item-heading > .small, -.list-group-item.active:focus .list-group-item-heading > .small { - color: inherit; -} -.list-group-item.active .list-group-item-text, -.list-group-item.active:hover .list-group-item-text, -.list-group-item.active:focus .list-group-item-text { - color: #e1edf7; -} -.list-group-item-success { - color: #3c763d; - background-color: #dff0d8; -} -a.list-group-item-success { - color: #3c763d; -} -a.list-group-item-success .list-group-item-heading { - color: inherit; -} -a.list-group-item-success:hover, -a.list-group-item-success:focus { - color: #3c763d; - background-color: #d0e9c6; -} -a.list-group-item-success.active, -a.list-group-item-success.active:hover, -a.list-group-item-success.active:focus { - color: #fff; - background-color: #3c763d; - border-color: #3c763d; -} -.list-group-item-info { - color: #31708f; - background-color: #d9edf7; -} -a.list-group-item-info { - color: #31708f; -} -a.list-group-item-info .list-group-item-heading { - color: inherit; -} -a.list-group-item-info:hover, -a.list-group-item-info:focus { - color: #31708f; - background-color: #c4e3f3; -} -a.list-group-item-info.active, -a.list-group-item-info.active:hover, -a.list-group-item-info.active:focus { - color: #fff; - background-color: #31708f; - border-color: #31708f; -} -.list-group-item-warning { - color: #8a6d3b; - background-color: #fcf8e3; -} -a.list-group-item-warning { - color: #8a6d3b; -} -a.list-group-item-warning .list-group-item-heading { - color: inherit; -} -a.list-group-item-warning:hover, -a.list-group-item-warning:focus { - color: #8a6d3b; - background-color: #faf2cc; -} -a.list-group-item-warning.active, -a.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus { - color: #fff; - background-color: #8a6d3b; - border-color: #8a6d3b; -} -.list-group-item-danger { - color: #a94442; - background-color: #f2dede; -} -a.list-group-item-danger { - color: #a94442; -} -a.list-group-item-danger .list-group-item-heading { - color: inherit; -} -a.list-group-item-danger:hover, -a.list-group-item-danger:focus { - color: #a94442; - background-color: #ebcccc; -} -a.list-group-item-danger.active, -a.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus { - color: #fff; - background-color: #a94442; - border-color: #a94442; -} -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; -} -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; -} -.panel { - margin-bottom: 20px; - background-color: #fff; - border: 1px solid transparent; - border-radius: 4px; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: 0 1px 1px rgba(0, 0, 0, .05); -} -.panel-body { - padding: 15px; -} -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel-heading > .dropdown .dropdown-toggle { - color: inherit; -} -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 16px; - color: inherit; -} -.panel-title > a { - color: inherit; -} -.panel-footer { - padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .list-group { - margin-bottom: 0; -} -.panel > .list-group .list-group-item { - border-width: 1px 0; - border-radius: 0; -} -.panel > .list-group:first-child .list-group-item:first-child { - border-top: 0; - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .list-group:last-child .list-group-item:last-child { - border-bottom: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel-heading + .list-group .list-group-item:first-child { - border-top-width: 0; -} -.list-group + .panel-footer { - border-top-width: 0; -} -.panel > .table, -.panel > .table-responsive > .table, -.panel > .panel-collapse > .table { - margin-bottom: 0; -} -.panel > .table:first-child, -.panel > .table-responsive:first-child > .table:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { - border-top-left-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { - border-top-right-radius: 3px; -} -.panel > .table:last-child, -.panel > .table-responsive:last-child > .table:last-child { - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { - border-bottom-right-radius: 3px; -} -.panel > .panel-body + .table, -.panel > .panel-body + .table-responsive { - border-top: 1px solid #ddd; -} -.panel > .table > tbody:first-child > tr:first-child th, -.panel > .table > tbody:first-child > tr:first-child td { - border-top: 0; -} -.panel > .table-bordered, -.panel > .table-responsive > .table-bordered { - border: 0; -} -.panel > .table-bordered > thead > tr > th:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, -.panel > .table-bordered > tbody > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, -.panel > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-bordered > thead > tr > td:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, -.panel > .table-bordered > tbody > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, -.panel > .table-bordered > tfoot > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; -} -.panel > .table-bordered > thead > tr > th:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, -.panel > .table-bordered > tbody > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, -.panel > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-bordered > thead > tr > td:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, -.panel > .table-bordered > tbody > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, -.panel > .table-bordered > tfoot > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; -} -.panel > .table-bordered > thead > tr:first-child > td, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, -.panel > .table-bordered > tbody > tr:first-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, -.panel > .table-bordered > thead > tr:first-child > th, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, -.panel > .table-bordered > tbody > tr:first-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { - border-bottom: 0; -} -.panel > .table-bordered > tbody > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, -.panel > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-bordered > tbody > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, -.panel > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { - border-bottom: 0; -} -.panel > .table-responsive { - margin-bottom: 0; - border: 0; -} -.panel-group { - margin-bottom: 20px; -} -.panel-group .panel { - margin-bottom: 0; - border-radius: 4px; -} -.panel-group .panel + .panel { - margin-top: 5px; -} -.panel-group .panel-heading { - border-bottom: 0; -} -.panel-group .panel-heading + .panel-collapse > .panel-body { - border-top: 1px solid #ddd; -} -.panel-group .panel-footer { - border-top: 0; -} -.panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid #ddd; -} -.panel-default { - border-color: #ddd; -} -.panel-default > .panel-heading { - color: #333; - background-color: #f5f5f5; - border-color: #ddd; -} -.panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ddd; -} -.panel-default > .panel-heading .badge { - color: #f5f5f5; - background-color: #333; -} -.panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ddd; -} -.panel-primary { - border-color: #428bca; -} -.panel-primary > .panel-heading { - color: #fff; - background-color: #428bca; - border-color: #428bca; -} -.panel-primary > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #428bca; -} -.panel-primary > .panel-heading .badge { - color: #428bca; - background-color: #fff; -} -.panel-primary > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #428bca; -} -.panel-success { - border-color: #d6e9c6; -} -.panel-success > .panel-heading { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; -} -.panel-success > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #d6e9c6; -} -.panel-success > .panel-heading .badge { - color: #dff0d8; - background-color: #3c763d; -} -.panel-success > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #d6e9c6; -} -.panel-info { - border-color: #bce8f1; -} -.panel-info > .panel-heading { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; -} -.panel-info > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #bce8f1; -} -.panel-info > .panel-heading .badge { - color: #d9edf7; - background-color: #31708f; -} -.panel-info > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #bce8f1; -} -.panel-warning { - border-color: #faebcc; -} -.panel-warning > .panel-heading { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; -} -.panel-warning > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #faebcc; -} -.panel-warning > .panel-heading .badge { - color: #fcf8e3; - background-color: #8a6d3b; -} -.panel-warning > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #faebcc; -} -.panel-danger { - border-color: #ebccd1; -} -.panel-danger > .panel-heading { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; -} -.panel-danger > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ebccd1; -} -.panel-danger > .panel-heading .badge { - color: #f2dede; - background-color: #a94442; -} -.panel-danger > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ebccd1; -} -.embed-responsive { - position: relative; - display: block; - height: 0; - padding: 0; - overflow: hidden; -} -.embed-responsive .embed-responsive-item, -.embed-responsive iframe, -.embed-responsive embed, -.embed-responsive object { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 100%; - height: 100%; - border: 0; -} -.embed-responsive.embed-responsive-16by9 { - padding-bottom: 56.25%; -} -.embed-responsive.embed-responsive-4by3 { - padding-bottom: 75%; -} -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); -} -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, .15); -} -.well-lg { - padding: 24px; - border-radius: 6px; -} -.well-sm { - padding: 9px; - border-radius: 3px; -} -.close { - float: right; - font-size: 21px; - font-weight: bold; - line-height: 1; - color: #000; - text-shadow: 0 1px 0 #fff; - filter: alpha(opacity=20); - opacity: .2; -} -.close:hover, -.close:focus { - color: #000; - text-decoration: none; - cursor: pointer; - filter: alpha(opacity=50); - opacity: .5; -} -button.close { - -webkit-appearance: none; - padding: 0; - cursor: pointer; - background: transparent; - border: 0; -} -.modal-open { - overflow: hidden; -} -.modal { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1050; - display: none; - overflow: hidden; - -webkit-overflow-scrolling: touch; - outline: 0; -} -.modal.fade .modal-dialog { - -webkit-transition: -webkit-transform .3s ease-out; - -o-transition: -o-transform .3s ease-out; - transition: transform .3s ease-out; - -webkit-transform: translate3d(0, -25%, 0); - -o-transform: translate3d(0, -25%, 0); - transform: translate3d(0, -25%, 0); -} -.modal.in .modal-dialog { - -webkit-transform: translate3d(0, 0, 0); - -o-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); -} -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; -} -.modal-dialog { - position: relative; - width: auto; - margin: 10px; -} -.modal-content { - position: relative; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - outline: 0; - -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); - box-shadow: 0 3px 9px rgba(0, 0, 0, .5); -} -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000; -} -.modal-backdrop.fade { - filter: alpha(opacity=0); - opacity: 0; -} -.modal-backdrop.in { - filter: alpha(opacity=50); - opacity: .5; -} -.modal-header { - min-height: 16.42857143px; - padding: 15px; - border-bottom: 1px solid #e5e5e5; -} -.modal-header .close { - margin-top: -2px; -} -.modal-title { - margin: 0; - line-height: 1.42857143; -} -.modal-body { - position: relative; - padding: 15px; -} -.modal-footer { - padding: 15px; - text-align: right; - border-top: 1px solid #e5e5e5; -} -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; -} -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; -} -@media (min-width: 768px) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - .modal-content { - -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); - box-shadow: 0 5px 15px rgba(0, 0, 0, .5); - } - .modal-sm { - width: 300px; - } -} -@media (min-width: 992px) { - .modal-lg { - width: 900px; - } -} -.tooltip { - position: absolute; - z-index: 1070; - display: block; - font-size: 12px; - line-height: 1.4; - visibility: visible; - filter: alpha(opacity=0); - opacity: 0; -} -.tooltip.in { - filter: alpha(opacity=90); - opacity: .9; -} -.tooltip.top { - padding: 5px 0; - margin-top: -3px; -} -.tooltip.right { - padding: 0 5px; - margin-left: 3px; -} -.tooltip.bottom { - padding: 5px 0; - margin-top: 3px; -} -.tooltip.left { - padding: 0 5px; - margin-left: -3px; -} -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #fff; - text-align: center; - text-decoration: none; - background-color: #000; - border-radius: 4px; -} -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.top-left .tooltip-arrow { - bottom: 0; - left: 5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.top-right .tooltip-arrow { - right: 5px; - bottom: 0; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: #000; -} -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: #000; -} -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.tooltip.bottom-left .tooltip-arrow { - top: 0; - left: 5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.tooltip.bottom-right .tooltip-arrow { - top: 0; - right: 5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - text-align: left; - white-space: normal; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - box-shadow: 0 5px 10px rgba(0, 0, 0, .2); -} -.popover.top { - margin-top: -10px; -} -.popover.right { - margin-left: 10px; -} -.popover.bottom { - margin-top: 10px; -} -.popover.left { - margin-left: -10px; -} -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - font-weight: normal; - line-height: 18px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - border-radius: 5px 5px 0 0; -} -.popover-content { - padding: 9px 14px; -} -.popover > .arrow, -.popover > .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.popover > .arrow { - border-width: 11px; -} -.popover > .arrow:after { - content: ""; - border-width: 10px; -} -.popover.top > .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, .25); - border-bottom-width: 0; -} -.popover.top > .arrow:after { - bottom: 1px; - margin-left: -10px; - content: " "; - border-top-color: #fff; - border-bottom-width: 0; -} -.popover.right > .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, .25); - border-left-width: 0; -} -.popover.right > .arrow:after { - bottom: -10px; - left: 1px; - content: " "; - border-right-color: #fff; - border-left-width: 0; -} -.popover.bottom > .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, .25); -} -.popover.bottom > .arrow:after { - top: 1px; - margin-left: -10px; - content: " "; - border-top-width: 0; - border-bottom-color: #fff; -} -.popover.left > .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, .25); -} -.popover.left > .arrow:after { - right: 1px; - bottom: -10px; - content: " "; - border-right-width: 0; - border-left-color: #fff; -} -.carousel { - position: relative; -} -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} -.carousel-inner > .item { - position: relative; - display: none; - -webkit-transition: .6s ease-in-out left; - -o-transition: .6s ease-in-out left; - transition: .6s ease-in-out left; -} -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - line-height: 1; -} -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} -.carousel-inner > .active { - left: 0; -} -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} -.carousel-inner > .next { - left: 100%; -} -.carousel-inner > .prev { - left: -100%; -} -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} -.carousel-inner > .active.left { - left: -100%; -} -.carousel-inner > .active.right { - left: 100%; -} -.carousel-control { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 15%; - font-size: 20px; - color: #fff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, .6); - filter: alpha(opacity=50); - opacity: .5; -} -.carousel-control.left { - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); - background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); - background-repeat: repeat-x; -} -.carousel-control.right { - right: 0; - left: auto; - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); - background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); - background-repeat: repeat-x; -} -.carousel-control:hover, -.carousel-control:focus { - color: #fff; - text-decoration: none; - filter: alpha(opacity=90); - outline: 0; - opacity: .9; -} -.carousel-control .icon-prev, -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-left, -.carousel-control .glyphicon-chevron-right { - position: absolute; - top: 50%; - z-index: 5; - display: inline-block; -} -.carousel-control .icon-prev, -.carousel-control .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; -} -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; -} -.carousel-control .icon-prev, -.carousel-control .icon-next { - width: 20px; - height: 20px; - margin-top: -10px; - font-family: serif; -} -.carousel-control .icon-prev:before { - content: '\2039'; -} -.carousel-control .icon-next:before { - content: '\203a'; -} -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - padding-left: 0; - margin-left: -30%; - text-align: center; - list-style: none; -} -.carousel-indicators li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - cursor: pointer; - background-color: #000 \9; - background-color: rgba(0, 0, 0, 0); - border: 1px solid #fff; - border-radius: 10px; -} -.carousel-indicators .active { - width: 12px; - height: 12px; - margin: 0; - background-color: #fff; -} -.carousel-caption { - position: absolute; - right: 15%; - bottom: 20px; - left: 15%; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: #fff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, .6); -} -.carousel-caption .btn { - text-shadow: none; -} -@media screen and (min-width: 768px) { - .carousel-control .glyphicon-chevron-left, - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-prev, - .carousel-control .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; - } - .carousel-control .glyphicon-chevron-left, - .carousel-control .icon-prev { - margin-left: -15px; - } - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-next { - margin-right: -15px; - } - .carousel-caption { - right: 20%; - left: 20%; - padding-bottom: 30px; - } - .carousel-indicators { - bottom: 20px; - } -} -.clearfix:before, -.clearfix:after, -.dl-horizontal dd:before, -.dl-horizontal dd:after, -.container:before, -.container:after, -.container-fluid:before, -.container-fluid:after, -.row:before, -.row:after, -.form-horizontal .form-group:before, -.form-horizontal .form-group:after, -.btn-toolbar:before, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:before, -.btn-group-vertical > .btn-group:after, -.nav:before, -.nav:after, -.navbar:before, -.navbar:after, -.navbar-header:before, -.navbar-header:after, -.navbar-collapse:before, -.navbar-collapse:after, -.pager:before, -.pager:after, -.panel-body:before, -.panel-body:after, -.modal-footer:before, -.modal-footer:after { - display: table; - content: " "; -} -.clearfix:after, -.dl-horizontal dd:after, -.container:after, -.container-fluid:after, -.row:after, -.form-horizontal .form-group:after, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:after, -.nav:after, -.navbar:after, -.navbar-header:after, -.navbar-collapse:after, -.pager:after, -.panel-body:after, -.modal-footer:after { - clear: both; -} -.center-block { - display: block; - margin-right: auto; - margin-left: auto; -} -.pull-right { - float: right !important; -} -.pull-left { - float: left !important; -} -.hide { - display: none !important; -} -.show { - display: block !important; -} -.invisible { - visibility: hidden; -} -.text-hide { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} -.hidden { - display: none !important; - visibility: hidden !important; -} -.affix { - position: fixed; - -webkit-transform: translate3d(0, 0, 0); - -o-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); -} -@-ms-viewport { - width: device-width; -} -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - display: none !important; -} -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} -@media (max-width: 767px) { - .visible-xs { - display: block !important; - } - table.visible-xs { - display: table; - } - tr.visible-xs { - display: table-row !important; - } - th.visible-xs, - td.visible-xs { - display: table-cell !important; - } -} -@media (max-width: 767px) { - .visible-xs-block { - display: block !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline { - display: inline !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline-block { - display: inline-block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm { - display: block !important; - } - table.visible-sm { - display: table; - } - tr.visible-sm { - display: table-row !important; - } - th.visible-sm, - td.visible-sm { - display: table-cell !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-block { - display: block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline { - display: inline !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline-block { - display: inline-block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md { - display: block !important; - } - table.visible-md { - display: table; - } - tr.visible-md { - display: table-row !important; - } - th.visible-md, - td.visible-md { - display: table-cell !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-block { - display: block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline { - display: inline !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline-block { - display: inline-block !important; - } -} -@media (min-width: 1200px) { - .visible-lg { - display: block !important; - } - table.visible-lg { - display: table; - } - tr.visible-lg { - display: table-row !important; - } - th.visible-lg, - td.visible-lg { - display: table-cell !important; - } -} -@media (min-width: 1200px) { - .visible-lg-block { - display: block !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline { - display: inline !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline-block { - display: inline-block !important; - } -} -@media (max-width: 767px) { - .hidden-xs { - display: none !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .hidden-sm { - display: none !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .hidden-md { - display: none !important; - } -} -@media (min-width: 1200px) { - .hidden-lg { - display: none !important; - } -} -.visible-print { - display: none !important; -} -@media print { - .visible-print { - display: block !important; - } - table.visible-print { - display: table; - } - tr.visible-print { - display: table-row !important; - } - th.visible-print, - td.visible-print { - display: table-cell !important; - } -} -.visible-print-block { - display: none !important; -} -@media print { - .visible-print-block { - display: block !important; - } -} -.visible-print-inline { - display: none !important; -} -@media print { - .visible-print-inline { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; -} -@media print { - .visible-print-inline-block { - display: inline-block !important; - } -} -@media print { - .hidden-print { - display: none !important; - } -} diff --git a/gui/slick/css/lib/bootstrap.min.css b/gui/slick/css/lib/bootstrap.min.css new file mode 100644 index 00000000..ed3905e0 --- /dev/null +++ b/gui/slick/css/lib/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/gui/slick/css/lib/jquery-ui-1.10.4.custom.css b/gui/slick/css/lib/jquery-ui-1.10.4.custom.css deleted file mode 100644 index dbf5897a..00000000 --- a/gui/slick/css/lib/jquery-ui-1.10.4.custom.css +++ /dev/null @@ -1,873 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-02-03 -* http://jqueryui.com -* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.tabs.css, jquery.ui.theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=%23ffffff&bgTextureHeader=flat&bgImgOpacityHeader=0&borderColorHeader=%23aaaaaa&fcHeader=%23222222&iconColorHeader=%23222222&bgColorContent=%23dcdcdc&bgTextureContent=highlight_soft&bgImgOpacityContent=75&borderColorContent=%23aaaaaa&fcContent=%23222222&iconColorContent=%23222222&bgColorDefault=%23efefef&bgTextureDefault=highlight_soft&bgImgOpacityDefault=75&borderColorDefault=%23aaaaaa&fcDefault=%23222222&iconColorDefault=%238c291d&bgColorHover=%23dddddd&bgTextureHover=highlight_soft&bgImgOpacityHover=75&borderColorHover=%23999999&fcHover=%23222222&iconColorHover=%23222222&bgColorActive=%23dfdfdf&bgTextureActive=inset_soft&bgImgOpacityActive=75&borderColorActive=%23aaaaaa&fcActive=%23140f06&iconColorActive=%238c291d&bgColorHighlight=%23fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=%23aaaaaa&fcHighlight=%23363636&iconColorHighlight=%232e83ff&bgColorError=%23fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=%23aaaaaa&fcError=%238c291d&iconColorError=%23cd0a0a&bgColorOverlay=%23aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=35&bgColorShadow=%23000000&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=35&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px&ctl=themeroller -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -/* Layout helpers -----------------------------------*/ -.ui-helper-hidden { - display: none; -} -.ui-helper-hidden-accessible { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} -.ui-helper-reset { - margin: 0; - padding: 0; - border: 0; - outline: 0; - line-height: 1.3; - text-decoration: none; - font-size: 100%; - list-style: none; -} -.ui-helper-clearfix:before, -.ui-helper-clearfix:after { - content: ""; - display: table; - border-collapse: collapse; -} -.ui-helper-clearfix:after { - clear: both; -} -.ui-helper-clearfix { - min-height: 0; /* support: IE7 */ -} -.ui-helper-zfix { - width: 100%; - height: 100%; - top: 0; - left: 0; - position: absolute; - opacity: 0; - filter:Alpha(Opacity=0); -} - -.ui-front { - z-index: 100; -} - - -/* Interaction Cues -----------------------------------*/ -.ui-state-disabled { - cursor: default !important; -} - - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - display: block; - text-indent: -99999px; - overflow: hidden; - background-repeat: no-repeat; -} - - -/* Misc visuals -----------------------------------*/ - -/* Overlays */ -.ui-widget-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.ui-resizable { - position: relative; -} -.ui-resizable-handle { - position: absolute; - font-size: 0.1px; - display: block; -} -.ui-resizable-disabled .ui-resizable-handle, -.ui-resizable-autohide .ui-resizable-handle { - display: none; -} -.ui-resizable-n { - cursor: n-resize; - height: 7px; - width: 100%; - top: -5px; - left: 0; -} -.ui-resizable-s { - cursor: s-resize; - height: 7px; - width: 100%; - bottom: -5px; - left: 0; -} -.ui-resizable-e { - cursor: e-resize; - width: 7px; - right: -5px; - top: 0; - height: 100%; -} -.ui-resizable-w { - cursor: w-resize; - width: 7px; - left: -5px; - top: 0; - height: 100%; -} -.ui-resizable-se { - cursor: se-resize; - width: 12px; - height: 12px; - right: 1px; - bottom: 1px; -} -.ui-resizable-sw { - cursor: sw-resize; - width: 9px; - height: 9px; - left: -5px; - bottom: -5px; -} -.ui-resizable-nw { - cursor: nw-resize; - width: 9px; - height: 9px; - left: -5px; - top: -5px; -} -.ui-resizable-ne { - cursor: ne-resize; - width: 9px; - height: 9px; - right: -5px; - top: -5px; -} -.ui-selectable-helper { - position: absolute; - z-index: 100; - border: 1px dotted black; -} -.ui-autocomplete { - position: absolute; - top: 0; - left: 0; - cursor: default; -} -.ui-button { - display: inline-block; - position: relative; - padding: 0; - line-height: normal; - margin-right: .1em; - cursor: pointer; - vertical-align: middle; - text-align: center; - overflow: visible; /* removes extra width in IE */ -} -.ui-button, -.ui-button:link, -.ui-button:visited, -.ui-button:hover, -.ui-button:active { - text-decoration: none; -} -/* to make room for the icon, a width needs to be set here */ -.ui-button-icon-only { - width: 2.2em; -} -/* button elements seem to need a little more width */ -button.ui-button-icon-only { - width: 2.4em; -} -.ui-button-icons-only { - width: 3.4em; -} -button.ui-button-icons-only { - width: 3.7em; -} - -/* button text element */ -.ui-button .ui-button-text { - display: block; - line-height: normal; -} -.ui-button-text-only .ui-button-text { - padding: .4em 1em; -} -.ui-button-icon-only .ui-button-text, -.ui-button-icons-only .ui-button-text { - padding: .4em; - text-indent: -9999999px; -} -.ui-button-text-icon-primary .ui-button-text, -.ui-button-text-icons .ui-button-text { - padding: .4em 1em .4em 2.1em; -} -.ui-button-text-icon-secondary .ui-button-text, -.ui-button-text-icons .ui-button-text { - padding: .4em 2.1em .4em 1em; -} -.ui-button-text-icons .ui-button-text { - padding-left: 2.1em; - padding-right: 2.1em; -} -/* no icon support for input elements, provide padding by default */ -input.ui-button { - padding: .4em 1em; -} - -/* button icon element(s) */ -.ui-button-icon-only .ui-icon, -.ui-button-text-icon-primary .ui-icon, -.ui-button-text-icon-secondary .ui-icon, -.ui-button-text-icons .ui-icon, -.ui-button-icons-only .ui-icon { - position: absolute; - top: 50%; - margin-top: -8px; -} -.ui-button-icon-only .ui-icon { - left: 50%; - margin-left: -8px; -} -.ui-button-text-icon-primary .ui-button-icon-primary, -.ui-button-text-icons .ui-button-icon-primary, -.ui-button-icons-only .ui-button-icon-primary { - left: .5em; -} -.ui-button-text-icon-secondary .ui-button-icon-secondary, -.ui-button-text-icons .ui-button-icon-secondary, -.ui-button-icons-only .ui-button-icon-secondary { - right: .5em; -} - -/* button sets */ -.ui-buttonset { - margin-right: 7px; -} -.ui-buttonset .ui-button { - margin-left: 0; - margin-right: -.3em; -} - -/* workarounds */ -/* reset extra padding in Firefox, see h5bp.com/l */ -input.ui-button::-moz-focus-inner, -button.ui-button::-moz-focus-inner { - border: 0; - padding: 0; -} -.ui-dialog { - overflow: hidden; - position: absolute; - top: 0; - left: 0; - padding: .2em; - outline: 0; -} -.ui-dialog .ui-dialog-titlebar { - padding: .4em 1em; - position: relative; -} -.ui-dialog .ui-dialog-title { - float: left; - margin: .1em 0; - white-space: nowrap; - width: 90%; - overflow: hidden; - text-overflow: ellipsis; -} -.ui-dialog .ui-dialog-titlebar-close { - position: absolute; - right: .3em; - top: 50%; - width: 20px; - margin: -10px 0 0 0; - padding: 1px; - height: 20px; -} -.ui-dialog .ui-dialog-content { - position: relative; - border: 0; - padding: .5em 1em; - background: none; - overflow: auto; -} -.ui-dialog .ui-dialog-buttonpane { - text-align: left; - border-width: 1px 0 0 0; - background-image: none; - margin-top: .5em; - padding: .3em 1em .5em .4em; -} -.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { - float: right; -} -.ui-dialog .ui-dialog-buttonpane button { - margin: .5em .4em .5em 0; - cursor: pointer; -} -.ui-dialog .ui-resizable-se { - width: 12px; - height: 12px; - right: -5px; - bottom: -5px; - background-position: 16px 16px; -} -.ui-draggable .ui-dialog-titlebar { - cursor: move; -} -.ui-menu { - list-style: none; - padding: 2px; - margin: 0; - display: block; - outline: none; -} -.ui-menu .ui-menu { - margin-top: -3px; - position: absolute; -} -.ui-menu .ui-menu-item { - margin: 0; - padding: 0; - width: 100%; - /* support: IE10, see #8844 */ - list-style-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7); -} -.ui-menu .ui-menu-divider { - margin: 5px -2px 5px -2px; - height: 0; - font-size: 0; - line-height: 0; - border-width: 1px 0 0 0; -} -.ui-menu .ui-menu-item a { - text-decoration: none; - display: block; - padding: 2px .4em; - line-height: 1.5; - min-height: 0; /* support: IE7 */ - font-weight: normal; -} -.ui-menu .ui-menu-item a.ui-state-focus, -.ui-menu .ui-menu-item a.ui-state-active { - font-weight: normal; - margin: -1px; -} - -.ui-menu .ui-state-disabled { - font-weight: normal; - margin: .4em 0 .2em; - line-height: 1.5; -} -.ui-menu .ui-state-disabled a { - cursor: default; -} - -/* icon support */ -.ui-menu-icons { - position: relative; -} -.ui-menu-icons .ui-menu-item a { - position: relative; - padding-left: 2em; -} - -/* left-aligned */ -.ui-menu .ui-icon { - position: absolute; - top: .2em; - left: .2em; -} - -/* right-aligned */ -.ui-menu .ui-menu-icon { - position: static; - float: right; -} -.ui-progressbar { - height: 2em; - text-align: left; - overflow: hidden; -} -.ui-progressbar .ui-progressbar-value { - margin: -1px; - height: 100%; -} -.ui-progressbar .ui-progressbar-overlay { - /* background: url("images/animated-overlay.gif"); */ - height: 100%; - filter: alpha(opacity=25); - opacity: 0.25; -} -.ui-progressbar-indeterminate .ui-progressbar-value { - background-image: none; -} -.ui-tabs { - position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ - padding: .2em; -} -.ui-tabs .ui-tabs-nav { - margin: 0; - padding: .2em .2em 0; -} -.ui-tabs .ui-tabs-nav li { - list-style: none; - float: left; - position: relative; - top: 0; - margin: 1px .2em 0 0; - border-bottom-width: 0; - padding: 0; - white-space: nowrap; -} -.ui-tabs .ui-tabs-nav .ui-tabs-anchor { - float: left; - padding: .5em 1em; - text-decoration: none; -} -.ui-tabs .ui-tabs-nav li.ui-tabs-active { - margin-bottom: -1px; - padding-bottom: 1px; -} -.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, -.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, -.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { - cursor: text; -} -.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { - cursor: pointer; -} -.ui-tabs .ui-tabs-panel { - display: block; - border-width: 0; - background: none; -} - -/* Component containers -----------------------------------*/ -.ui-widget { - -} -.ui-widget .ui-widget { - -} -.ui-widget input, -.ui-widget select, -.ui-widget textarea, -.ui-widget button { - -} -.ui-widget-content { - border: 1px solid #aaaaaa; - /* background: #dcdcdc url(images/ui-bg_highlight-soft_75_dcdcdc_1x100.png) 50% top repeat-x; */ - color: #222222; -} -.ui-widget-content a { - color: #222222; -} -.ui-widget-header { - border: 1px solid #aaaaaa; - /* background: #ffffff url(images/ui-bg_flat_0_ffffff_40x100.png) 50% 50% repeat-x; */ - color: #222222; - font-weight: bold; -} -.ui-widget-header a { - color: #222222; -} - -/* Interaction states -----------------------------------*/ -.ui-state-default, -.ui-widget-content .ui-state-default, -.ui-widget-header .ui-state-default { - border: 1px solid #aaaaaa; - /* background: #efefef url(images/ui-bg_highlight-soft_75_efefef_1x100.png) 50% 50% repeat-x; */ - font-weight: bold; - color: #222222; -} -.ui-state-default a, -.ui-state-default a:link, -.ui-state-default a:visited { - color: #222222; - text-decoration: none; -} -.ui-state-hover, -.ui-widget-content .ui-state-hover, -.ui-widget-header .ui-state-hover, -.ui-state-focus, -.ui-widget-content .ui-state-focus, -.ui-widget-header .ui-state-focus { - border: 1px solid #999999; - /* background: #dddddd url(images/ui-bg_highlight-soft_75_dddddd_1x100.png) 50% 50% repeat-x; */ - font-weight: bold; - color: #222222; -} -.ui-state-hover a, -.ui-state-hover a:hover, -.ui-state-hover a:link, -.ui-state-hover a:visited, -.ui-state-focus a, -.ui-state-focus a:hover, -.ui-state-focus a:link, -.ui-state-focus a:visited { - color: #222222; - text-decoration: none; -} -.ui-state-active, -.ui-widget-content .ui-state-active, -.ui-widget-header .ui-state-active { - border: 1px solid #aaaaaa; - background: #dfdfdf url(images/ui-bg_inset-soft_75_dfdfdf_1x100.png) 50% 50% repeat-x; - font-weight: bold; - color: #140f06; -} -.ui-state-active a, -.ui-state-active a:link, -.ui-state-active a:visited { - color: #140f06; - text-decoration: none; -} - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, -.ui-widget-content .ui-state-highlight, -.ui-widget-header .ui-state-highlight { - border: 1px solid #aaaaaa; - /* background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; */ - color: #363636; -} -.ui-state-highlight a, -.ui-widget-content .ui-state-highlight a, -.ui-widget-header .ui-state-highlight a { - color: #363636; -} -.ui-state-error, -.ui-widget-content .ui-state-error, -.ui-widget-header .ui-state-error { - border: 1px solid #aaaaaa; - /* background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; */ - color: #8c291d; -} -.ui-state-error a, -.ui-widget-content .ui-state-error a, -.ui-widget-header .ui-state-error a { - color: #8c291d; -} -.ui-state-error-text, -.ui-widget-content .ui-state-error-text, -.ui-widget-header .ui-state-error-text { - color: #8c291d; -} -.ui-priority-primary, -.ui-widget-content .ui-priority-primary, -.ui-widget-header .ui-priority-primary { - font-weight: bold; -} -.ui-priority-secondary, -.ui-widget-content .ui-priority-secondary, -.ui-widget-header .ui-priority-secondary { - opacity: .7; - filter:Alpha(Opacity=70); - font-weight: normal; -} -.ui-state-disabled, -.ui-widget-content .ui-state-disabled, -.ui-widget-header .ui-state-disabled { - opacity: .35; - filter:Alpha(Opacity=35); - background-image: none; -} -.ui-state-disabled .ui-icon { - filter:Alpha(Opacity=35); /* For IE8 - See #6059 */ -} - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - width: 16px; - height: 16px; -} -/* -.ui-icon, -.ui-widget-content .ui-icon { - background-image: url(images/ui-icons_222222_256x240.png); -} -.ui-widget-header .ui-icon { - background-image: url(images/ui-icons_222222_256x240.png); -} -.ui-state-default .ui-icon { - background-image: url(images/ui-icons_8c291d_256x240.png); -} -.ui-state-hover .ui-icon, -.ui-state-focus .ui-icon { - background-image: url(images/ui-icons_222222_256x240.png); -} -.ui-state-active .ui-icon { - background-image: url(images/ui-icons_8c291d_256x240.png); -} -.ui-state-highlight .ui-icon { - background-image: url(images/ui-icons_2e83ff_256x240.png); -} -.ui-state-error .ui-icon, -.ui-state-error-text .ui-icon { - background-image: url(images/ui-icons_cd0a0a_256x240.png); -} -*/ - -/* positioning */ -.ui-icon-blank { background-position: 16px 16px; } -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background-position: -16px -144px; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-on { background-position: -96px -144px; } -.ui-icon-radio-off { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-start { background-position: -80px -160px; } -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-all, -.ui-corner-top, -.ui-corner-left, -.ui-corner-tl { - border-top-left-radius: 4px; -} -.ui-corner-all, -.ui-corner-top, -.ui-corner-right, -.ui-corner-tr { - border-top-right-radius: 4px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-left, -.ui-corner-bl { - border-bottom-left-radius: 4px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-right, -.ui-corner-br { - border-bottom-right-radius: 4px; -} - -/* Overlays */ -.ui-widget-overlay { - /* background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; */ - opacity: .35; - filter: Alpha(Opacity=35); -} -.ui-widget-shadow { - margin: -8px 0 0 -8px; - padding: 8px; - /* background: #000000 url(images/ui-bg_flat_0_000000_40x100.png) 50% 50% repeat-x; */ - opacity: .35; - filter: Alpha(Opacity=35); - border-radius: 8px; -} - -.ui-tooltip { - padding:3px 6px; - position:absolute; - z-index:9999; - max-width:300px; - - -webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15); - -moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15); - box-shadow:1px 1px 3px 1px rgba(0,0,0,.15) -} -body .ui-tooltip { - font-size: 10px; - border-radius: 5px 5px 5px 5px; - border: 1px solid rgb(241, 208, 49); - background-color: rgb(255, 255, 163); - color: rgb(85, 85, 85) -} \ No newline at end of file diff --git a/gui/slick/css/lib/jquery-ui.min.css b/gui/slick/css/lib/jquery-ui.min.css new file mode 100644 index 00000000..e9080541 --- /dev/null +++ b/gui/slick/css/lib/jquery-ui.min.css @@ -0,0 +1,3 @@ +/*! jQuery UI - v1.12.1 - 2017-01-31 +* http://jqueryui.com +* Copyright jQuery Foundation and other contributors; Licensed MIT */.ui-button-icon-only,.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-checkboxradio-disabled,.ui-state-disabled{pointer-events:none}.ui-helper-reset,.ui-menu{outline:0;list-style:none}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;line-height:1.3;text-decoration:none;font-size:100%}.ui-helper-clearfix:after,.ui-helper-clearfix:before{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-autohide .ui-resizable-handle,.ui-resizable-disabled .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{padding:0;margin:0;display:block}.ui-button,.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0}.ui-menu .ui-state-active,.ui-menu .ui-state-focus{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:active,.ui-button:hover,.ui-button:link,.ui-button:visited{text-decoration:none}.ui-button-icon-only{width:2em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}button.ui-button::-moz-focus-inner,input.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup>.ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup>.ui-controlgroup-item.ui-visual-focus,.ui-controlgroup>.ui-controlgroup-item:focus{z-index:9999}.ui-controlgroup-vertical>.ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label+.ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label+.ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc(100% - 2.4em)}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:0 0;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw,.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url(data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==);height:100%;filter:alpha(opacity=25);opacity:.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:700;line-height:1.5;padding:2px .4em;margin:.5em 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:0 0}.ui-widget-content,.ui-widget-header,.ui-widget.ui-widget-content{border:1px solid #aaa}.ui-widget-content,.ui-widget-content a{color:#222}.ui-widget-header{background:#fff;color:#222;font-weight:700}.ui-widget-header a{color:#222}.ui-button,.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,html .ui-button.ui-state-disabled:active,html .ui-button.ui-state-disabled:hover{border:1px solid #aaa;font-weight:400;color:#222}.ui-button,.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button{color:#222;text-decoration:none}.ui-button:focus,.ui-button:hover,.ui-state-focus,.ui-state-hover,.ui-widget-content .ui-state-focus,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-focus,.ui-widget-header .ui-state-hover{border:1px solid #999;font-weight:400;color:#222}.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,a.ui-button:focus,a.ui-button:hover{color:#222;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px #5e9ed6}.ui-button.ui-state-active:hover,.ui-button:active,.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active{border:1px solid #aaa;font-weight:400;color:#140f06}.ui-icon-background,.ui-state-active .ui-icon-background{border:#aaa;background-color:#140f06}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#140f06;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #aaa;color:#363636}.ui-state-checked{border:1px solid #aaa;background:#fbf9ee}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #aaa;color:#8c291d}.ui-state-error a,.ui-state-error-text,.ui-widget-content .ui-state-error a,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error a,.ui-widget-header .ui-state-error-text{color:#8c291d}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:700}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:400}.ui-state-disabled .ui-icon,.ui-widget-overlay{filter:Alpha(Opacity=35)}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-icon{width:16px;height:16px}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-first,.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-left,.ui-corner-tl,.ui-corner-top{border-top-left-radius:4px}.ui-corner-all,.ui-corner-right,.ui-corner-top,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bl,.ui-corner-bottom,.ui-corner-left{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-br,.ui-corner-right{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa;opacity:.35}.ui-widget-shadow{-webkit-box-shadow:-8px -8px 8px #000;box-shadow:-8px -8px 8px #000} \ No newline at end of file diff --git a/gui/slick/css/lib/jquery.qtip-2.2.1.min.css b/gui/slick/css/lib/jquery.qtip-2.2.1.min.css deleted file mode 100644 index ac501ed1..00000000 --- a/gui/slick/css/lib/jquery.qtip-2.2.1.min.css +++ /dev/null @@ -1,3 +0,0 @@ -/* qTip2 v2.2.1 | Plugins: tips viewport imagemap svg modal ie6 | Styles: core basic css3 | qtip2.com | Licensed MIT | Sat Sep 06 2014 18:25:07 */ - -.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;direction:ltr;box-shadow:none;padding:0}.qtip-content{position:relative;padding:5px 9px;overflow:hidden;text-align:left;word-wrap:break-word}.qtip-titlebar{position:relative;padding:5px 35px 5px 10px;overflow:hidden;border-width:0 0 1px;font-weight:700}.qtip-titlebar+.qtip-content{border-top-width:0!important}.qtip-close{position:absolute;right:-9px;top:-9px;z-index:11;cursor:pointer;outline:medium none;border:1px solid transparent}.qtip-titlebar .qtip-close{right:4px;top:50%;margin-top:-9px}* html .qtip-titlebar .qtip-close{top:16px}.qtip-titlebar .ui-icon,.qtip-icon .ui-icon{display:block;text-indent:-1000em;direction:ltr}.qtip-icon,.qtip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.qtip-icon .ui-icon{width:18px;height:14px;line-height:14px;text-align:center;text-indent:0;font:400 bold 10px/13px Tahoma,sans-serif;color:inherit;background:transparent none no-repeat -100em -100em}.qtip-focus{}.qtip-hover{}.qtip-default{border:1px solid #F1D031;background-color:#FFFFA3;color:#555}.qtip-default .qtip-titlebar{background-color:#FFEF93}.qtip-default .qtip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.qtip-default .qtip-titlebar .qtip-close{border-color:#AAA;color:#111} .qtip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.qtip-light .qtip-titlebar{background-color:#f1f1f1} .qtip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.qtip-dark .qtip-titlebar{background-color:#404040}.qtip-dark .qtip-icon{border-color:#444}.qtip-dark .qtip-titlebar .ui-state-hover{border-color:#303030} .qtip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.qtip-cream .qtip-titlebar{background-color:#F0DE7D}.qtip-cream .qtip-close .qtip-icon{background-position:-82px 0} .qtip-red{background-color:#F78B83;border-color:#D95252;color:#912323}.qtip-red .qtip-titlebar{background-color:#F06D65}.qtip-red .qtip-close .qtip-icon{background-position:-102px 0}.qtip-red .qtip-icon{border-color:#D95252}.qtip-red .qtip-titlebar .ui-state-hover{border-color:#D95252} .qtip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.qtip-green .qtip-titlebar{background-color:#B0DE78}.qtip-green .qtip-close .qtip-icon{background-position:-42px 0} .qtip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.qtip-blue .qtip-titlebar{background-color:#D0E9F5}.qtip-blue .qtip-close .qtip-icon{background-position:-2px 0}.qtip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.qtip-rounded,.qtip-tipsy,.qtip-bootstrap{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.qtip-rounded .qtip-titlebar{-moz-border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.qtip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border:0 solid transparent;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,#000));background-image:-webkit-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-moz-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-ms-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-o-linear-gradient(top,#4A4A4A 0,#000 100%)}.qtip-youtube .qtip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.qtip-youtube .qtip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);"}.qtip-youtube .qtip-icon{border-color:#222}.qtip-youtube .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-linear-gradient(top,#717171,#232323);background-image:-ms-linear-gradient(top,#717171,#232323);background-image:-o-linear-gradient(top,#717171,#232323);border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.qtip-jtools .qtip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A)"}.qtip-jtools .qtip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323)"}.qtip-jtools .qtip-titlebar,.qtip-jtools .qtip-content{background:transparent;color:#fff;border:0 dashed transparent}.qtip-jtools .qtip-icon{border-color:#555}.qtip-jtools .qtip-titlebar .ui-state-hover{border-color:#333}.qtip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.qtip-cluetip .qtip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.qtip-cluetip .qtip-icon{border-color:#808064}.qtip-cluetip .qtip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.qtip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px #000}.qtip-tipsy .qtip-titlebar{padding:6px 35px 0 10px;background-color:transparent}.qtip-tipsy .qtip-content{padding:6px 10px}.qtip-tipsy .qtip-icon{border-color:#222;text-shadow:none}.qtip-tipsy .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.qtip-tipped .qtip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from(#3A79B8),to(#2E629D));background-image:-webkit-linear-gradient(top,#3A79B8,#2E629D);background-image:-moz-linear-gradient(top,#3A79B8,#2E629D);background-image:-ms-linear-gradient(top,#3A79B8,#2E629D);background-image:-o-linear-gradient(top,#3A79B8,#2E629D);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D)"}.qtip-tipped .qtip-icon{border:2px solid #285589;background:#285589}.qtip-tipped .qtip-icon .ui-icon{background-color:#FBFBFB;color:#555}.qtip-bootstrap{font-size:14px;line-height:20px;color:#333;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.qtip-bootstrap .qtip-titlebar{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.qtip-bootstrap .qtip-titlebar .qtip-close{right:11px;top:45%;border-style:none}.qtip-bootstrap .qtip-content{padding:9px 14px}.qtip-bootstrap .qtip-icon{background:transparent}.qtip-bootstrap .qtip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.qtip-bootstrap .qtip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.qtip:not(.ie9haxors) div.qtip-content,.qtip:not(.ie9haxors) div.qtip-titlebar{filter:none;-ms-filter:none}.qtip .qtip-tip{margin:0 auto;overflow:hidden;z-index:10}x:-o-prefocus,.qtip .qtip-tip{visibility:hidden}.qtip .qtip-tip,.qtip .qtip-tip .qtip-vml,.qtip .qtip-tip canvas{position:absolute;color:#123456;background:transparent;border:0 dashed transparent}.qtip .qtip-tip canvas{top:0;left:0}.qtip .qtip-tip .qtip-vml{behavior:url(#default#VML);display:inline-block;visibility:visible}#qtip-overlay{position:fixed;left:0;top:0;width:100%;height:100%}#qtip-overlay.blurs{cursor:pointer}#qtip-overlay div{position:absolute;left:0;top:0;width:100%;height:100%;background-color:#000;opacity:.7;filter:alpha(opacity=70);-ms-filter:"alpha(Opacity=70)"}.qtipmodal-ie6fix{position:absolute!important} \ No newline at end of file diff --git a/gui/slick/css/lib/jquery.qtip.min.css b/gui/slick/css/lib/jquery.qtip.min.css new file mode 100644 index 00000000..5adc4b75 --- /dev/null +++ b/gui/slick/css/lib/jquery.qtip.min.css @@ -0,0 +1 @@ +#qtip-overlay.blurs,.qtip-close{cursor:pointer}.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;direction:ltr;box-shadow:none;padding:0}.qtip-content,.qtip-titlebar{position:relative;overflow:hidden}.qtip-content{padding:5px 9px;text-align:left;word-wrap:break-word}.qtip-titlebar{padding:5px 35px 5px 10px;border-width:0 0 1px;font-weight:700}.qtip-titlebar+.qtip-content{border-top-width:0!important}.qtip-close{position:absolute;right:-9px;top:-9px;z-index:11;outline:0;border:1px solid transparent}.qtip-titlebar .qtip-close{right:4px;top:50%;margin-top:-9px}* html .qtip-titlebar .qtip-close{top:16px}.qtip-icon .ui-icon,.qtip-titlebar .ui-icon{display:block;text-indent:-1000em;direction:ltr}.qtip-icon,.qtip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.qtip-icon .ui-icon{width:18px;height:14px;line-height:14px;text-align:center;text-indent:0;font:normal 700 10px/13px Tahoma,sans-serif;color:inherit;background:-100em -100em no-repeat}.qtip-default{border:1px solid #F1D031;background-color:#FFFFA3;color:#555}.qtip-default .qtip-titlebar{background-color:#FFEF93}.qtip-default .qtip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.qtip-default .qtip-titlebar .qtip-close{border-color:#AAA;color:#111}.qtip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.qtip-light .qtip-titlebar{background-color:#f1f1f1}.qtip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.qtip-dark .qtip-titlebar{background-color:#404040}.qtip-dark .qtip-icon{border-color:#444}.qtip-dark .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.qtip-red,.qtip-red .qtip-icon,.qtip-red .qtip-titlebar .ui-state-hover{border-color:#D95252}.qtip-cream .qtip-titlebar{background-color:#F0DE7D}.qtip-cream .qtip-close .qtip-icon{background-position:-82px 0}.qtip-red{background-color:#F78B83;color:#912323}.qtip-red .qtip-titlebar{background-color:#F06D65}.qtip-red .qtip-close .qtip-icon{background-position:-102px 0}.qtip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.qtip-green .qtip-titlebar{background-color:#B0DE78}.qtip-green .qtip-close .qtip-icon{background-position:-42px 0}.qtip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.qtip-blue .qtip-titlebar{background-color:#D0E9F5}.qtip-blue .qtip-close .qtip-icon{background-position:-2px 0}.qtip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.qtip-bootstrap,.qtip-rounded,.qtip-tipsy{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.qtip-rounded .qtip-titlebar{-moz-border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.qtip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border:0 solid transparent;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,#000));background-image:-webkit-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-moz-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-ms-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-o-linear-gradient(top,#4A4A4A 0,#000 100%)}.qtip-youtube .qtip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.qtip-youtube .qtip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"}.qtip-youtube .qtip-icon{border-color:#222}.qtip-youtube .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-linear-gradient(top,#717171,#232323);background-image:-ms-linear-gradient(top,#717171,#232323);background-image:-o-linear-gradient(top,#717171,#232323);border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.qtip-jtools .qtip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"}.qtip-jtools .qtip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"}.qtip-jtools .qtip-content,.qtip-jtools .qtip-titlebar{background:0 0;color:#fff;border:0 dashed transparent}.qtip-jtools .qtip-icon{border-color:#555}.qtip-jtools .qtip-titlebar .ui-state-hover{border-color:#333}.qtip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.qtip-cluetip .qtip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.qtip-cluetip .qtip-icon{border-color:#808064}.qtip-cluetip .qtip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.qtip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px #000}.qtip-tipsy .qtip-titlebar{padding:6px 35px 0 10px;background-color:transparent}.qtip-tipsy .qtip-content{padding:6px 10px}.qtip-tipsy .qtip-icon{border-color:#222;text-shadow:none}.qtip-tipsy .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.qtip-tipped .qtip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from(#3A79B8),to(#2E629D));background-image:-webkit-linear-gradient(top,#3A79B8,#2E629D);background-image:-moz-linear-gradient(top,#3A79B8,#2E629D);background-image:-ms-linear-gradient(top,#3A79B8,#2E629D);background-image:-o-linear-gradient(top,#3A79B8,#2E629D);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"}.qtip-tipped .qtip-icon{border:2px solid #285589;background:#285589}.qtip-tipped .qtip-icon .ui-icon{background-color:#FBFBFB;color:#555}.qtip-bootstrap{font-size:14px;line-height:20px;color:#333;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.qtip-bootstrap .qtip-titlebar{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.qtip-bootstrap .qtip-titlebar .qtip-close{right:11px;top:45%;border-style:none}.qtip-bootstrap .qtip-content{padding:9px 14px}.qtip-bootstrap .qtip-icon{background:0 0}.qtip-bootstrap .qtip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}#qtip-overlay,#qtip-overlay div{left:0;top:0;width:100%;height:100%}.qtip-bootstrap .qtip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.qtip:not(.ie9haxors) div.qtip-content,.qtip:not(.ie9haxors) div.qtip-titlebar{filter:none;-ms-filter:none}.qtip .qtip-tip{margin:0 auto;overflow:hidden;z-index:10}.qtip .qtip-tip,x:-o-prefocus{visibility:hidden}.qtip .qtip-tip,.qtip .qtip-tip .qtip-vml,.qtip .qtip-tip canvas{position:absolute;color:#123456;background:0 0;border:0 dashed transparent}.qtip .qtip-tip canvas{top:0;left:0}.qtip .qtip-tip .qtip-vml{behavior:url(#default#VML);display:inline-block;visibility:visible}#qtip-overlay{position:fixed}#qtip-overlay div{position:absolute;background-color:#000;opacity:.7;filter:alpha(opacity=70);-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"}.qtipmodal-ie6fix{position:absolute!important} \ No newline at end of file diff --git a/gui/slick/css/lib/pnotify.custom.min.css b/gui/slick/css/lib/pnotify.custom.min.css index 0cf49ca7..a2e81ec7 100644 --- a/gui/slick/css/lib/pnotify.custom.min.css +++ b/gui/slick/css/lib/pnotify.custom.min.css @@ -1 +1 @@ -.ui-pnotify{top:25px;right:25px;position:absolute;height:auto;z-index:9999}html>body>.ui-pnotify{position:fixed}.ui-pnotify .ui-pnotify-shadow{-webkit-box-shadow:0 2px 10px rgba(50,50,50,.5);-moz-box-shadow:0 2px 10px rgba(50,50,50,.5);box-shadow:0 2px 10px rgba(50,50,50,.5)}.ui-pnotify-container{background-position:0 0;padding:.8em;height:100%;margin:0}.ui-pnotify-container.ui-pnotify-sharp{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.ui-pnotify-title{display:block;margin-bottom:.4em;margin-top:0}.ui-pnotify-text{display:block}.ui-pnotify-icon,.ui-pnotify-icon span{display:block;float:left;margin-right:.2em}.ui-pnotify.stack-bottomleft,.ui-pnotify.stack-topleft{left:25px;right:auto}.ui-pnotify.stack-bottomleft,.ui-pnotify.stack-bottomright{bottom:25px;top:auto}.ui-pnotify-closer,.ui-pnotify-sticker{float:right;margin-left:.2em}.ui-pnotify-history-container{position:absolute;top:0;right:18px;width:70px;border-top:none;padding:0;-webkit-border-top-left-radius:0;-moz-border-top-left-radius:0;border-top-left-radius:0;-webkit-border-top-right-radius:0;-moz-border-top-right-radius:0;border-top-right-radius:0;z-index:10000}.ui-pnotify-history-container.ui-pnotify-history-fixed{position:fixed}.ui-pnotify-history-container .ui-pnotify-history-header{padding:2px;text-align:center}.ui-pnotify-history-container button{cursor:pointer;display:block;width:100%}.ui-pnotify-history-container .ui-pnotify-history-pulldown{display:block;margin:0 auto} \ No newline at end of file +.ui-pnotify{top:36px;right:36px;position:absolute;height:auto;z-index:2}body>.ui-pnotify{position:fixed;z-index:100040}.ui-pnotify-modal-overlay{background-color:rgba(0,0,0,.4);top:0;left:0;position:absolute;height:100%;width:100%;z-index:1}body>.ui-pnotify-modal-overlay{position:fixed;z-index:100039}.ui-pnotify.ui-pnotify-in{display:block!important}.ui-pnotify.ui-pnotify-move{transition:left .5s ease,top .5s ease,right .5s ease,bottom .5s ease}.ui-pnotify.ui-pnotify-fade-slow{transition:opacity .6s linear;opacity:0}.ui-pnotify.ui-pnotify-fade-slow.ui-pnotify.ui-pnotify-move{transition:opacity .6s linear,left .5s ease,top .5s ease,right .5s ease,bottom .5s ease}.ui-pnotify.ui-pnotify-fade-normal{transition:opacity .4s linear;opacity:0}.ui-pnotify.ui-pnotify-fade-normal.ui-pnotify.ui-pnotify-move{transition:opacity .4s linear,left .5s ease,top .5s ease,right .5s ease,bottom .5s ease}.ui-pnotify.ui-pnotify-fade-fast{transition:opacity .2s linear;opacity:0}.ui-pnotify.ui-pnotify-fade-fast.ui-pnotify.ui-pnotify-move{transition:opacity .2s linear,left .5s ease,top .5s ease,right .5s ease,bottom .5s ease}.ui-pnotify.ui-pnotify-fade-in{opacity:1}.ui-pnotify .ui-pnotify-shadow{-webkit-box-shadow:0 6px 28px 0 rgba(0,0,0,.1);-moz-box-shadow:0 6px 28px 0 rgba(0,0,0,.1);box-shadow:0 6px 28px 0 rgba(0,0,0,.1)}.ui-pnotify-container{background-position:0 0;padding:.8em;height:100%;margin:0}.ui-pnotify-container:after{content:" ";visibility:hidden;display:block;height:0;clear:both}.ui-pnotify-container.ui-pnotify-sharp{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.ui-pnotify-title{display:block;margin-bottom:.4em;margin-top:0}.ui-pnotify-text{display:block}.ui-pnotify-icon,.ui-pnotify-icon span{display:block;float:left;margin-right:.2em}.ui-pnotify.stack-bottomleft,.ui-pnotify.stack-topleft{left:25px;right:auto}.ui-pnotify.stack-bottomleft,.ui-pnotify.stack-bottomright{bottom:25px;top:auto}.ui-pnotify.stack-modal{left:50%;right:auto;margin-left:-150px}.brighttheme{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.brighttheme.ui-pnotify-container{padding:18px}.brighttheme .ui-pnotify-title{margin-bottom:12px}.brighttheme-notice{background-color:#FFFFA2;border:0 solid #FF0;color:#4F4F00}.brighttheme-info{background-color:#8FCEDD;border:0 solid #0286A5;color:#012831}.brighttheme-success{background-color:#AFF29A;border:0 solid #35DB00;color:#104300}.brighttheme-error{background-color:#FFABA2;background-image:repeating-linear-gradient(135deg,transparent,transparent 35px,rgba(255,255,255,.3) 35px,rgba(255,255,255,.3) 70px);border:0 solid #FF1800;color:#4F0800}.brighttheme-icon-closer,.brighttheme-icon-info,.brighttheme-icon-notice,.brighttheme-icon-sticker,.brighttheme-icon-success{position:relative;width:16px;height:16px;font-size:12px;font-weight:700;line-height:16px;font-family:"Courier New",Courier,monospace;border-radius:50%}.brighttheme-icon-closer:after,.brighttheme-icon-info:after,.brighttheme-icon-notice:after,.brighttheme-icon-sticker:after,.brighttheme-icon-success:after{position:absolute;top:0;left:4px}.brighttheme-icon-notice{background-color:#2E2E00;color:#FFFFA2;margin-top:2px}.brighttheme-icon-notice:after{content:"!"}.brighttheme-icon-info{background-color:#012831;color:#8FCEDD;margin-top:2px}.brighttheme-icon-info:after{content:"i"}.brighttheme-icon-success{background-color:#104300;color:#AFF29A;margin-top:2px}.brighttheme-icon-success:after{content:"\002713"}.brighttheme-icon-error{position:relative;width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-bottom:16px solid #2E0400;font-size:0;line-height:0;color:#FFABA2;margin-top:1px}.brighttheme-icon-error:after{position:absolute;top:1px;left:-4px;font-size:12px;font-weight:700;line-height:16px;font-family:"Courier New",Courier,monospace;content:"!"}.brighttheme-icon-closer,.brighttheme-icon-sticker{display:inline-block}.brighttheme-icon-closer:after{top:-4px;content:"\002715"}.brighttheme-icon-sticker:after{top:-5px;content:"\01D1BC";-moz-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.brighttheme-icon-sticker.brighttheme-icon-stuck:after{-moz-transform:rotate(180deg);-webkit-transform:rotate(180deg);-o-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.brighttheme .ui-pnotify-action-bar{padding-top:12px}.brighttheme .ui-pnotify-action-bar input,.brighttheme .ui-pnotify-action-bar textarea{display:block;width:100%;margin-bottom:12px!important}.brighttheme .ui-pnotify-action-button{text-transform:uppercase;font-weight:700;padding:4px 8px;border:none;background:0 0}.brighttheme .ui-pnotify-action-button.btn-primary{border:none;border-radius:0}.brighttheme-notice .ui-pnotify-action-button.btn-primary{background-color:#FF0;color:#4F4F00}.brighttheme-info .ui-pnotify-action-button.btn-primary{background-color:#0286A5;color:#012831}.brighttheme-success .ui-pnotify-action-button.btn-primary{background-color:#35DB00;color:#104300}.brighttheme-error .ui-pnotify-action-button.btn-primary{background-color:#FF1800;color:#4F0800}.ui-pnotify-closer,.ui-pnotify-sticker{float:right;margin-left:.2em}.ui-pnotify-history-container{position:absolute;top:0;right:18px;width:70px;border-top:none;padding:0;-webkit-border-top-left-radius:0;-moz-border-top-left-radius:0;border-top-left-radius:0;-webkit-border-top-right-radius:0;-moz-border-top-right-radius:0;border-top-right-radius:0;z-index:10000}.ui-pnotify-history-container.ui-pnotify-history-fixed{position:fixed}.ui-pnotify-history-container .ui-pnotify-history-header{padding:2px;text-align:center}.ui-pnotify-history-container button{cursor:pointer;display:block;width:100%}.ui-pnotify-history-container .ui-pnotify-history-pulldown{display:block;margin:0 auto}.ui-pnotify-container{position:relative;left:0}@media (max-width:480px){.ui-pnotify-mobile-able.ui-pnotify{position:fixed;top:0;right:0;left:0;width:auto!important;font-size:1.2em;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased;-ms-font-smoothing:antialiased;font-smoothing:antialiased}.ui-pnotify-mobile-able.ui-pnotify .ui-pnotify-shadow{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-bottom-width:5px}.ui-pnotify-mobile-able .ui-pnotify-container{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.ui-pnotify-mobile-able.ui-pnotify.stack-bottomleft,.ui-pnotify-mobile-able.ui-pnotify.stack-topleft{left:0;right:0}.ui-pnotify-mobile-able.ui-pnotify.stack-bottomleft,.ui-pnotify-mobile-able.ui-pnotify.stack-bottomright{left:0;right:0;bottom:0;top:auto}.ui-pnotify-mobile-able.ui-pnotify.stack-bottomleft .ui-pnotify-shadow,.ui-pnotify-mobile-able.ui-pnotify.stack-bottomright .ui-pnotify-shadow{border-top-width:5px;border-bottom-width:1px}}.ui-pnotify.ui-pnotify-nonblock-fade{opacity:.2}.ui-pnotify.ui-pnotify-nonblock-hide{display:none!important}@import url(https://fonts.googleapis.com/css?family=Material+Icons);.pnotify-material{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;font-size:14px}.pnotify-material.ui-pnotify-shadow{-webkit-box-shadow:0 6px 24px 0 rgba(0,0,0,.2);-moz-box-shadow:0 6px 24px 0 rgba(0,0,0,.2);box-shadow:0 6px 24px 0 rgba(0,0,0,.2)}.pnotify-material.ui-pnotify-container{padding:24px}.pnotify-material .ui-pnotify-title{margin-bottom:20px;font-size:20px}.pnotify-material-notice{background-color:#FFEB3B;border:none;color:rgba(0,0,0,.87)}.pnotify-material-info{background-color:#2196F3;border:none;color:#fff}.pnotify-material-success{background-color:#8BC34A;border:none;color:rgba(0,0,0,.87)}.pnotify-material-error{background-color:#F44336;border:none;color:#fff}.pnotify-material-icon-closer,.pnotify-material-icon-info,.pnotify-material-icon-notice,.pnotify-material-icon-sticker,.pnotify-material-icon-success{position:relative;width:16px;height:16px;font-size:12px;font-weight:700;line-height:16px;font-family:"Courier New",Courier,monospace;border-radius:50%}.pnotify-material-icon-closer:after,.pnotify-material-icon-info:after,.pnotify-material-icon-notice:after,.pnotify-material-icon-sticker:after,.pnotify-material-icon-success:after{position:absolute;top:0;left:4px}.pnotify-material-icon-notice:after{content:"announcement"}.pnotify-material-icon-info:after{content:"info"}.pnotify-material-icon-success:after{content:"check circle"}.pnotify-material-icon-error:after{content:"report problem"}.pnotify-material-icon-closer,.pnotify-material-icon-sticker{display:inline-block}.pnotify-material-icon-closer:after{top:-4px;content:"\002715"}.pnotify-material-icon-sticker:after{top:-5px;content:"\01D1BC";-moz-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.pnotify-material-icon-sticker.pnotify-material-icon-stuck:after{-moz-transform:rotate(180deg);-webkit-transform:rotate(180deg);-o-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)} \ No newline at end of file diff --git a/gui/slick/css/lib/token-input.min.css b/gui/slick/css/lib/token-input.min.css new file mode 100644 index 00000000..16f33eb6 --- /dev/null +++ b/gui/slick/css/lib/token-input.min.css @@ -0,0 +1 @@ +div.token-input-dropdown,ul.token-input-list{overflow:hidden;font-size:12px;font-family:Verdana,sans-serif}ul.token-input-list,ul.token-input-list li{list-style-type:none}ul.token-input-list{height:auto!important;height:1%;width:400px;border:1px solid #999;cursor:text;z-index:999;margin:0;padding:0;background-color:#fff;clear:left}ul.token-input-list li input{border:0;width:350px;padding:3px 8px;background-color:#fff;-webkit-appearance:caret}ul.token-input-disabled,ul.token-input-disabled li input{background-color:#E8E8E8}ul.token-input-disabled li.token-input-token{background-color:#D9E3CA;color:#7D7D7D}ul.token-input-disabled li.token-input-token span{color:#CFCFCF;cursor:default}li.token-input-token{overflow:hidden;height:auto!important;height:1%;margin:3px;padding:3px 5px;background-color:#d0efa0;color:#000;font-weight:700;cursor:default;display:block}li.token-input-token p{float:left;padding:0;margin:0}li.token-input-token span{float:right;color:#777;cursor:pointer}li.token-input-selected-token{background-color:#08844e;color:#fff}li.token-input-selected-token span{color:#bbb}div.token-input-dropdown{position:absolute;width:400px;background-color:#fff;border-left:1px solid #ccc;border-right:1px solid #ccc;border-bottom:1px solid #ccc;cursor:default;z-index:1}div.token-input-dropdown p{margin:0;padding:5px;font-weight:700;color:#777}div.token-input-dropdown ul{margin:0;padding:0}div.token-input-dropdown ul li{background-color:#fff;padding:3px;list-style-type:none}div.token-input-dropdown ul li.token-input-dropdown-item{background-color:#fafafa}div.token-input-dropdown ul li.token-input-dropdown-item2{background-color:#fff}div.token-input-dropdown ul li em{font-weight:700;font-style:normal}div.token-input-dropdown ul li.token-input-selected-dropdown-item{background-color:#d0efa0} \ No newline at end of file diff --git a/gui/slick/css/light.css b/gui/slick/css/light.css index 94b04bd8..f060f85f 100644 --- a/gui/slick/css/light.css +++ b/gui/slick/css/light.css @@ -1,21 +1,25 @@ /* ======================================================================= inc_top.tmpl ========================================================================== */ +.shows-not-found.n .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.n, pre .prelight{ color:#6f8c53 } +.shows-not-found.nn .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.nn, pre .prelight2{ color:#b7b82c } +.shows-not-found.nnn .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.nnn, pre .prelight-num{ color:#eaab52 } +.shows-not-found.nnnn .snf .sgicon-warning, .navbar-default .navbar-nav .logger.errors.nnnn{ color:#ff6d5e } @@ -56,6 +60,10 @@ pre .prelight-num{ background:#dcdcdc url("../css/lib/images/ui-bg_highlight-soft_75_dcdcdc_1x100.png") 50% top repeat-x } +.ui-widget.ui-widget-content{ + border-color:#fff +} + .ui-widget-content a{ color:rgb(42, 100, 150) } @@ -69,12 +77,18 @@ pre .prelight-num{ } .ui-state-default, +.ui-widget.ui-button, +.ui-widget.ui-button:active, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default{ background:#fff; border:1px solid #ccc } +.ui-widget.ui-button:hover{ + border-color:#ccc +} + .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, @@ -116,15 +130,19 @@ pre .prelight-num{ } .ui-state-hover .ui-icon, -.ui-state-focus .ui-icon{ +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon{ background-image:url("../css/lib/images/ui-icons_222222_256x240.png") } -.ui-state-active .ui-icon{ +.ui-state-active .ui-icon, +.ui-button:active .ui-icon{ background-image:url("../css/lib/images/ui-icons_8c291d_256x240.png") } -.ui-state-highlight .ui-icon{ +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon{ background-image:url("../css/lib/images/ui-icons_2e83ff_256x240.png") } @@ -133,6 +151,10 @@ pre .prelight-num{ background-image:url("../css/lib/images/ui-icons_cd0a0a_256x240.png") } +.ui-button .ui-icon{ + background-image:url("../css/lib/images/ui-icons_8c291d_256x240.png") +} + .ui-widget-overlay{ background:#aaa url("../css/lib/images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x } @@ -290,11 +312,13 @@ a.ui-font{ background-image:linear-gradient(to left, rgba(223, 218, 207, 1), rgba(223, 218, 207, 0)) } +.show-toggle-hide, td.tvShow a{ color:#000; text-decoration:none } +.show-toggle-hide:hover, td.tvShow a:hover span, td.tvShow a:hover{ cursor:pointer; @@ -609,10 +633,14 @@ config*.tmpl color:#666 } -.testNotification{ +.test-notification{ border:1px dotted #ccc } +.provider-enabled{ + box-shadow:-26px 0 0 0 rgb(199, 219, 64) inset +} + .infoTableSeperator{ border-top:1px dotted #666 } @@ -879,6 +907,7 @@ fieldset[disabled] .navbar-default .btn-link:focus{ background-color:#333 } +.component-group.typelist .bgcol, .dropdown-menu{ background-color:#f5f1e4; border:1px solid rgba(0, 0, 0, 0.15); @@ -1230,16 +1259,20 @@ input sizing (for config pages) ========================================================================== */ .showlist-select optgroup, +#results-sortby optgroup, #pickShow optgroup, #showfilter optgroup, +#showsort optgroup, #editAProvider optgroup{ color:#eee; background-color:#888 } .showlist-select optgroup option, +#results-sortby optgroup option, #pickShow optgroup option, #showfilter optgroup option, +#showsort optgroup option, #editAProvider optgroup option{ color:#222; background-color:#fff @@ -1382,66 +1415,6 @@ thead.tablesorter-stickyHeader{ color:#fff } -/* ======================================================================= -token-input.css -========================================================================== */ - -ul.token-input-list{ - border:1px solid #ccc; - background-color:#fff -} - -ul.token-input-list li input{ - border:0; - background-color:white -} - -li.token-input-token{ - background-color:#d0efa0; - color:#000 -} - -li.token-input-token span{ - color:#777 -} - -li.token-input-selected-token{ - background-color:#08844e; - color:#fff -} - -li.token-input-selected-token span{ - color:#bbb -} - -div.token-input-dropdown{ - background-color:#fff; - color:#000; - border-left-color:#ccc; - border-right-color:#ccc; - border-bottom-color:#ccc -} - -div.token-input-dropdown p{ - color:#777 -} - -div.token-input-dropdown ul li{ - background-color:#fff -} - -div.token-input-dropdown ul li.token-input-dropdown-item{ - background-color:#fafafa -} - -div.token-input-dropdown ul li.token-input-dropdown-item2{ - background-color:#fff -} - -div.token-input-dropdown ul li.token-input-selected-dropdown-item{ - background-color:#6196c2 -} - /* ======================================================================= jquery.confirm.css ========================================================================== */ diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css index 0b1a3da6..c970884d 100644 --- a/gui/slick/css/style.css +++ b/gui/slick/css/style.css @@ -161,6 +161,7 @@ inc_top.tmpl margin-bottom:-15px } +.navbar-default .navbar-nav .snf.bar, .navbar-default .navbar-nav .logger.bar{ position: absolute; display:none; @@ -169,18 +170,32 @@ inc_top.tmpl right:12px } +.navbar-default .navbar-nav .item .sgicon-showqueue, +.shows-not-found .navbar-default .navbar-nav .snf.item .sgicon-warning, +.shows-not-found.n .navbar-default .navbar-nav .snf.bar, .shows-not-found.nn .navbar-default .navbar-nav .snf.bar, +.shows-not-found.nnn .navbar-default .navbar-nav .snf.bar, .shows-not-found.nnnn .navbar-default .navbar-nav .snf.bar, +.n .navbar-default .navbar-nav .snf.item .sgicon-showqueue, .nn .navbar-default .navbar-nav .snf.item .sgicon-showqueue, +.nnn .navbar-default .navbar-nav .snf.item .sgicon-showqueue, .nnnn .navbar-default .navbar-nav .snf.item .sgicon-showqueue, .navbar-default .navbar-nav .logger.errors{ display:block; } +.navbar-default .navbar-nav .snf.item, .navbar-default .navbar-nav .logger.errors.item{ display:inline-block } +.navbar-default .navbar-nav .snf, .navbar-default .navbar-nav .logger{ height:14px } +.shows-not-found .navbar-default .navbar-nav .snf.bar, +.shows-not-found .navbar-default .navbar-nav .snf.item .sgicon-showqueue, +.navbar-default .navbar-nav .snf.item .sgicon-warning{ + display:none +} + [class^="icon-"], [class*=" icon-"]{ background-image:url("../images/glyphicons-halflings.png") @@ -217,6 +232,10 @@ inc_top.tmpl background:#dcdcdc url("../css/lib/images/ui-bg_highlight-soft_75_dcdcdc_1x100.png") 50% top repeat-x } +.ui-widget.ui-widget-content{ + border:1px solid +} + .ui-widget-content a{ text-decoration:none } @@ -225,6 +244,10 @@ inc_top.tmpl background:#ddd url("../css/lib/images/ui-bg_flat_0_ffffff_40x100.png") 50% 50% repeat-x } +.ui-dialog-buttonpane .ui-widget.ui-button{ + padding:0.8em 1.85em +} + .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default{ @@ -232,6 +255,10 @@ inc_top.tmpl border:1px solid #ccc } +.ui-widget.ui-button:hover{ + border:1px solid +} + .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, @@ -295,6 +322,13 @@ inc_top.tmpl } .ui-widget-shadow{ + margin:-8px 0 0 -8px; + padding:8px; + opacity:.35; + filter:alpha(opacity=35); + -webkit-border-radius:8px; + -moz-border-radius:8px; + border-radius:8px; background:#000 url("../css/lib/images/ui-bg_flat_0_000000_40x100.png") 50% 50% repeat-x } @@ -785,21 +819,27 @@ home.tmpl height:8px !important } -#show-list .show-card .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br{ +#show-list .show-card .ui-corner-all, #show-list .show-card .ui-corner-bottom, +#show-list .show-card .ui-corner-right, #show-list .show-card .ui-corner-br{ border-bottom-right-radius:5px } -#show-list .show-card .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl{ +#show-list .show-card .ui-corner-all, #show-list .show-card .ui-corner-bottom, +#show-list .show-card .ui-corner-left, #show-list .show-card .ui-corner-bl{ border-bottom-left-radius:5px } -#browse-list .show-card .ui-corner-all, -#show-list .show-card .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr{ +#browse-list .show-card .ui-corner-all, #browse-list .show-card .ui-corner-top, +#browse-list .show-card .ui-corner-right, #browse-list .show-card .ui-corner-tr, +#show-list .show-card .ui-corner-all, #show-list .show-card .ui-corner-top, +#show-list .show-card .ui-corner-right, #show-list .show-card .ui-corner-tr{ border-top-right-radius:0 } -#browse-list .show-card .ui-corner-all, -#show-list .show-card .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl{ +#browse-list .show-card .ui-corner-all, #browse-list .show-card .ui-corner-top, +#browse-list .show-card .ui-corner-left, #browse-list .show-card .ui-corner-tl, +#show-list .show-card .ui-corner-all, #show-list .show-card .ui-corner-top, +#show-list .show-card .ui-corner-left, #show-list .show-card .ui-corner-tl{ border-top-left-radius:0 } @@ -857,6 +897,12 @@ home.tmpl background-image:linear-gradient(to left, rgba(223, 218, 207, 1), rgba(223, 218, 207, 0)) } +.show-toggle-hide{ + position:absolute; + top:272px; + right:2px +} + .show-date{ position:relative; overflow:hidden; @@ -1082,11 +1128,16 @@ div.formpaginate{ margin-left:10px } -.stepDiv #searchResults div{ +.stepDiv #searchResults .results-item{ + width:100%; line-height:1.7 } -.stepDiv #searchResults div .exists-db{ +.stepDiv #searchResults .results-item input[disabled=disabled]{ + visibility:hidden +} + +.stepDiv #searchResults .results-item .exists-db{ font-weight:800; font-style:italic } @@ -1095,6 +1146,11 @@ div.formpaginate{ margin-right:6px } +#addShowForm a span.article, +#addShowForm a:hover span.article{ + color:#2f4799 +} + .stepone-result-title{ font-weight:600; margin-left:10px @@ -1780,6 +1836,25 @@ a.service img{ vertical-align:-2px } +.airdate-never, +#display-show .tablesorter tr.airdate-never{ + background-color:#eae2c8; + color:#666 +} +#display-show.back-art.pro.ii .tablesorter tr.airdate-never{ + background-color:rgba(234,226,200,0.7); + color:#666 +} + +.unaired, +#display-show .tablesorter tr.unaired{ + background-color:#f5f1e4 +} +#display-show.back-art.pro.ii .tablesorter tr.unaired{ + background-color:rgba(245,241,228,0.7); + color:#584b20 +} + .good, #display-show .tablesorter tr.good{ background-color:#c3e3c8 @@ -1840,25 +1915,6 @@ a.service img{ color:#295730 } -.airdate-never, -#display-show .tablesorter tr.airdate-never{ - background-color:#eae2c8; - color:#666 -} -#display-show.back-art.pro.ii .tablesorter tr.airdate-never{ - background-color:rgba(234,226,200,0.7); - color:#666 -} - -.unaired, -#display-show .tablesorter tr.unaired{ - background-color:#f5f1e4 -} -#display-show.back-art.pro.ii .tablesorter tr.unaired{ - background-color:rgba(245,241,228,0.7); - color:#584b20 -} - span.good{ color:#295730; border:1px solid #295730 @@ -2619,8 +2675,14 @@ config*.tmpl text-align:left } +.ui-tabs .ui-tabs-panel{ + padding-left:15px; + padding-right:15px +} + .component-group{ - padding:15px 15px 25px; + padding:0 0 13px; + margin: 0 0 12px; border-bottom:1px dotted #ccc; min-height:200px } @@ -2748,6 +2810,7 @@ config*.tmpl padding-top:10px } +select .selected-text, select .selected{ font-weight:700; color:#888 @@ -2776,7 +2839,7 @@ select .selected:before{ line-height:normal } -.testNotification{ +.test-notification{ padding:5px; margin-bottom:10px; line-height:20px; @@ -3348,6 +3411,18 @@ img[src=""],img:not([src]){ margin:-1% } +/* Fixes Firefox anomaly during image load */ +@-moz-document url-prefix(){ + img:-moz-loading{visibility:hidden} +} + +.lazy-loading-image{ + display:inline-block; + position:absolute; + top:0; + left:0 +} + /* ======================================================================= bootstrap Overrides ========================================================================== */ @@ -3390,11 +3465,6 @@ input, textarea, select, .uneditable-input{ padding:0 } -/* navbar styling */ -.back-art.pro.ii .navbar-default{ - background-image:none -} - .navbar-default .navbar-brand{ color:#ddd } @@ -3448,6 +3518,12 @@ input, textarea, select, .uneditable-input{ background-color:#333 } +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus, +.navbar-default .navbar-toggle .icon-bar{ + text-decoration:none +} + .navbar-default .navbar-collapse, .navbar-default .navbar-form{ border-color:#3e3f3a @@ -3460,6 +3536,21 @@ input, textarea, select, .uneditable-input{ color:#ddd } +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus, +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus, +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus, +.back-art.pro.ii .navbar-default, +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus{ + background-image:none; + text-decoration:none +} + @media(max-width:767px){ .navbar-default .navbar-nav .open .dropdown-menu > li > a{ color:#ddd @@ -4071,6 +4162,13 @@ fieldset.sectionwrap{ margin-top:10px } +.fullwidth, +.fullwidth div.stepsguide, +.fullwidth div.stepsguide .step{ + width:100%; + cursor:default +} + div.stepsguide .step, legend.legendStep{ margin-bottom:0 @@ -4276,58 +4374,25 @@ thead.tablesorter-stickyHeader{ } /* ======================================================================= -token-input.css +token-input.css Overrides ========================================================================== */ ul.token-input-list{ - overflow:hidden; - height:auto !important; - height:1%; width:273px; border:1px solid #ccc; - cursor:text; font-size:10px; - font-family:Verdana; - z-index:999; - margin:0; padding:0 0 1px 0; - background-color:#ddd; - list-style-type:none; -/* clear:left; */ - border-top-left-radius:3px; - border-top-right-radius:3px; - border-bottom-left-radius:3px; - border-bottom-right-radius:3px -} - -ul.token-input-list li{ - list-style-type:none + clear:none; + border-radius:3px } ul.token-input-list li input{ - border:0; padding:3px 4px; - background-color:white -/* -webkit-appearance:caret */ + -webkit-appearance:none } li.token-input-token{ - overflow:hidden; - height:auto !important; - height:1%; - margin:3px; - padding:3px 5px 0 5px; - background-color:#d0efa0; - color:#000; - font-weight:bold; - cursor:default; - display:block -} - -li.token-input-token img{ - padding-top:7px; - padding-right:4px; - float:left + padding:3px 5px 0 5px } li.token-input-token input{ @@ -4337,75 +4402,27 @@ li.token-input-token input{ } li.token-input-token p{ - float:left; - padding:0; - margin:0; line-height:2.0 !important } -li.token-input-token span{ - float:right; - color:#777; - cursor:pointer -} - -li.token-input-selected-token{ - background-color:#08844e; - color:#ddd -} - -li.token-input-selected-token span{ - color:#bbb -} - li.token-input-input-token input{ - margin:3px 3px 3px 3px !important + margin:3px !important } div.token-input-dropdown{ - position:absolute; width:273px; - overflow:hidden; - border-left:1px solid; - border-right:1px solid; - border-bottom:1px solid; - cursor:default; - font-size:11px; - font-family:Verdana; - z-index:1 + color:#000; + font-size:11px } div.token-input-dropdown p{ - margin:0; - padding:3px; - font-weight:bold; - color:#777 -} - -div.token-input-dropdown ul{ - margin:0; - padding:0 -} - -div.token-input-dropdown ul li{ - background-color:#ddd; - padding:3px; - list-style-type:none -} - -div.token-input-dropdown ul li.token-input-dropdown-item{ - background-color:#fafafa + padding:3px } div.token-input-dropdown ul li.token-input-dropdown-item2{ background-color:#ddd } -div.token-input-dropdown ul li em{ - font-weight:bold; - font-style:normal -} - div.token-input-dropdown ul li.token-input-selected-dropdown-item{ background-color:#6196c2 } @@ -4414,6 +4431,17 @@ span.token-input-delete-token{ margin:0 1px } +li.token-input-token img{ + padding:5px 4px 0 0; + float:left +} + +li.token-input-dropdown-item img, +li.token-input-dropdown-item2 img{ + padding:2px 4px 0 0; + float:left +} + /* ======================================================================= jquery.confirm.css ========================================================================== */ diff --git a/gui/slick/images/image-light.png b/gui/slick/images/image-light.png new file mode 100644 index 00000000..59e658b1 Binary files /dev/null and b/gui/slick/images/image-light.png differ diff --git a/gui/slick/images/network/axn.png b/gui/slick/images/network/axn.png new file mode 100644 index 00000000..b0b0001d Binary files /dev/null and b/gui/slick/images/network/axn.png differ diff --git a/gui/slick/images/network/bbc four.png b/gui/slick/images/network/bbc four.png index 51761ddc..221b9b36 100644 Binary files a/gui/slick/images/network/bbc four.png and b/gui/slick/images/network/bbc four.png differ diff --git a/gui/slick/images/network/bbc news.png b/gui/slick/images/network/bbc news.png new file mode 100644 index 00000000..ae59225c Binary files /dev/null and b/gui/slick/images/network/bbc news.png differ diff --git a/gui/slick/images/network/bbc one.png b/gui/slick/images/network/bbc one.png index cf6004cf..a23d45ec 100644 Binary files a/gui/slick/images/network/bbc one.png and b/gui/slick/images/network/bbc one.png differ diff --git a/gui/slick/images/network/bbc parliament.png b/gui/slick/images/network/bbc parliament.png new file mode 100644 index 00000000..ceb57a81 Binary files /dev/null and b/gui/slick/images/network/bbc parliament.png differ diff --git a/gui/slick/images/network/bbc three.png b/gui/slick/images/network/bbc three.png index e6ce9ec2..e53134dd 100644 Binary files a/gui/slick/images/network/bbc three.png and b/gui/slick/images/network/bbc three.png differ diff --git a/gui/slick/images/network/bbc two.png b/gui/slick/images/network/bbc two.png index 84266c4b..81014881 100644 Binary files a/gui/slick/images/network/bbc two.png and b/gui/slick/images/network/bbc two.png differ diff --git a/gui/slick/images/network/bravo.png b/gui/slick/images/network/bravo.png index 660165d4..9ba6cc7e 100644 Binary files a/gui/slick/images/network/bravo.png and b/gui/slick/images/network/bravo.png differ diff --git a/gui/slick/images/network/byu television.png b/gui/slick/images/network/byu television.png new file mode 100644 index 00000000..d0540f10 Binary files /dev/null and b/gui/slick/images/network/byu television.png differ diff --git a/gui/slick/images/network/cbbc.png b/gui/slick/images/network/cbbc.png index 423c83e3..0b96fbbc 100644 Binary files a/gui/slick/images/network/cbbc.png and b/gui/slick/images/network/cbbc.png differ diff --git a/gui/slick/images/network/cbs all access.png b/gui/slick/images/network/cbs all access.png new file mode 100644 index 00000000..95bd36ba Binary files /dev/null and b/gui/slick/images/network/cbs all access.png differ diff --git a/gui/slick/images/network/cbs.png b/gui/slick/images/network/cbs.png index f3e57474..563a5ad4 100644 Binary files a/gui/slick/images/network/cbs.png and b/gui/slick/images/network/cbs.png differ diff --git a/gui/slick/images/network/cmt.png b/gui/slick/images/network/cmt.png new file mode 100644 index 00000000..1fa4c089 Binary files /dev/null and b/gui/slick/images/network/cmt.png differ diff --git a/gui/slick/images/network/ion television.png b/gui/slick/images/network/ion television.png new file mode 100644 index 00000000..21a24875 Binary files /dev/null and b/gui/slick/images/network/ion television.png differ diff --git a/gui/slick/images/network/itv2.png b/gui/slick/images/network/itv2.png index f92bed0e..3473c0ff 100644 Binary files a/gui/slick/images/network/itv2.png and b/gui/slick/images/network/itv2.png differ diff --git a/gui/slick/images/network/much.png b/gui/slick/images/network/much.png new file mode 100644 index 00000000..00faf9f3 Binary files /dev/null and b/gui/slick/images/network/much.png differ diff --git a/gui/slick/images/network/nick jr..png b/gui/slick/images/network/nick jr..png new file mode 100644 index 00000000..567a5ae5 Binary files /dev/null and b/gui/slick/images/network/nick jr..png differ diff --git a/gui/slick/images/network/reelzchannel.png b/gui/slick/images/network/reelzchannel.png new file mode 100644 index 00000000..7c0d4a28 Binary files /dev/null and b/gui/slick/images/network/reelzchannel.png differ diff --git a/gui/slick/images/network/scifi.png b/gui/slick/images/network/scifi.png index b1e41f22..fd5ce1d9 100644 Binary files a/gui/slick/images/network/scifi.png and b/gui/slick/images/network/scifi.png differ diff --git a/gui/slick/images/network/stan.png b/gui/slick/images/network/stan.png new file mode 100644 index 00000000..b9b2e0af Binary files /dev/null and b/gui/slick/images/network/stan.png differ diff --git a/gui/slick/images/network/syfy.png b/gui/slick/images/network/syfy.png index fcb431a0..fd5ce1d9 100644 Binary files a/gui/slick/images/network/syfy.png and b/gui/slick/images/network/syfy.png differ diff --git a/gui/slick/images/network/tbs superstation.png b/gui/slick/images/network/tbs superstation.png index 98151e97..77643e09 100644 Binary files a/gui/slick/images/network/tbs superstation.png and b/gui/slick/images/network/tbs superstation.png differ diff --git a/gui/slick/images/network/tbs.png b/gui/slick/images/network/tbs.png index 98151e97..77643e09 100644 Binary files a/gui/slick/images/network/tbs.png and b/gui/slick/images/network/tbs.png differ diff --git a/gui/slick/images/network/teennick.png b/gui/slick/images/network/teennick.png new file mode 100644 index 00000000..1732f89e Binary files /dev/null and b/gui/slick/images/network/teennick.png differ diff --git a/gui/slick/images/network/usa network.png b/gui/slick/images/network/usa network.png index 8bec2afc..718f8923 100644 Binary files a/gui/slick/images/network/usa network.png and b/gui/slick/images/network/usa network.png differ diff --git a/gui/slick/images/network/usa.png b/gui/slick/images/network/usa.png index 8bec2afc..718f8923 100644 Binary files a/gui/slick/images/network/usa.png and b/gui/slick/images/network/usa.png differ diff --git a/gui/slick/images/notifiers/discordapp.png b/gui/slick/images/notifiers/discordapp.png new file mode 100644 index 00000000..cdd63bea Binary files /dev/null and b/gui/slick/images/notifiers/discordapp.png differ diff --git a/gui/slick/images/notifiers/email.png b/gui/slick/images/notifiers/email.png index ee3fa707..6a7c1637 100644 Binary files a/gui/slick/images/notifiers/email.png and b/gui/slick/images/notifiers/email.png differ diff --git a/gui/slick/images/notifiers/gitter.png b/gui/slick/images/notifiers/gitter.png new file mode 100644 index 00000000..d2a31aa3 Binary files /dev/null and b/gui/slick/images/notifiers/gitter.png differ diff --git a/gui/slick/images/notifiers/slack.png b/gui/slick/images/notifiers/slack.png new file mode 100644 index 00000000..f348c2d3 Binary files /dev/null and b/gui/slick/images/notifiers/slack.png differ diff --git a/gui/slick/images/notifiers/synologynotifier.png b/gui/slick/images/notifiers/synologynotifier.png index 6ec497da..ebe55b16 100644 Binary files a/gui/slick/images/notifiers/synologynotifier.png and b/gui/slick/images/notifiers/synologynotifier.png differ diff --git a/gui/slick/images/notifiers/twitter.png b/gui/slick/images/notifiers/twitter.png index 685697ae..8faaf38e 100644 Binary files a/gui/slick/images/notifiers/twitter.png and b/gui/slick/images/notifiers/twitter.png differ diff --git a/gui/slick/images/providers/blutopia.png b/gui/slick/images/providers/blutopia.png new file mode 100644 index 00000000..3abc97aa Binary files /dev/null and b/gui/slick/images/providers/blutopia.png differ diff --git a/gui/slick/images/providers/extratorrent.png b/gui/slick/images/providers/extratorrent.png deleted file mode 100644 index f4e49273..00000000 Binary files a/gui/slick/images/providers/extratorrent.png and /dev/null differ diff --git a/gui/slick/images/providers/freshontv.png b/gui/slick/images/providers/freshontv.png deleted file mode 100644 index 089d3479..00000000 Binary files a/gui/slick/images/providers/freshontv.png and /dev/null differ diff --git a/gui/slick/images/providers/magnetdl.png b/gui/slick/images/providers/magnetdl.png new file mode 100644 index 00000000..3040b1ad Binary files /dev/null and b/gui/slick/images/providers/magnetdl.png differ diff --git a/gui/slick/images/providers/nebulance.png b/gui/slick/images/providers/nebulance.png new file mode 100644 index 00000000..a0a3141f Binary files /dev/null and b/gui/slick/images/providers/nebulance.png differ diff --git a/gui/slick/images/providers/nyaa.png b/gui/slick/images/providers/nyaa.png new file mode 100644 index 00000000..5f3b2c37 Binary files /dev/null and b/gui/slick/images/providers/nyaa.png differ diff --git a/gui/slick/images/providers/nyaatorrents.png b/gui/slick/images/providers/nyaatorrents.png deleted file mode 100644 index afa0b197..00000000 Binary files a/gui/slick/images/providers/nyaatorrents.png and /dev/null differ diff --git a/gui/slick/images/providers/sceneaccess.png b/gui/slick/images/providers/sceneaccess.png deleted file mode 100644 index a250a2da..00000000 Binary files a/gui/slick/images/providers/sceneaccess.png and /dev/null differ diff --git a/gui/slick/images/providers/scenehd.png b/gui/slick/images/providers/scenehd.png new file mode 100644 index 00000000..d1f4da48 Binary files /dev/null and b/gui/slick/images/providers/scenehd.png differ diff --git a/gui/slick/images/providers/skytorrents.png b/gui/slick/images/providers/skytorrents.png new file mode 100644 index 00000000..3677d18f Binary files /dev/null and b/gui/slick/images/providers/skytorrents.png differ diff --git a/gui/slick/images/providers/torrentshack.png b/gui/slick/images/providers/torrentshack.png deleted file mode 100644 index f9ae440d..00000000 Binary files a/gui/slick/images/providers/torrentshack.png and /dev/null differ diff --git a/gui/slick/images/providers/torrentvault.png b/gui/slick/images/providers/torrentvault.png new file mode 100644 index 00000000..2d9ab670 Binary files /dev/null and b/gui/slick/images/providers/torrentvault.png differ diff --git a/gui/slick/images/providers/transmithe_net.png b/gui/slick/images/providers/transmithe_net.png deleted file mode 100644 index 2ccfcfaa..00000000 Binary files a/gui/slick/images/providers/transmithe_net.png and /dev/null differ diff --git a/gui/slick/images/providers/usenet_crawler.png b/gui/slick/images/providers/usenet_crawler.png index 5c48557d..a9db1287 100644 Binary files a/gui/slick/images/providers/usenet_crawler.png and b/gui/slick/images/providers/usenet_crawler.png differ diff --git a/gui/slick/images/providers/womble_s_index.png b/gui/slick/images/providers/womble_s_index.png deleted file mode 100644 index c4d3a8f1..00000000 Binary files a/gui/slick/images/providers/womble_s_index.png and /dev/null differ diff --git a/gui/slick/images/providers/wop.png b/gui/slick/images/providers/wop.png new file mode 100644 index 00000000..6053085e Binary files /dev/null and b/gui/slick/images/providers/wop.png differ diff --git a/gui/slick/interfaces/default/apiBuilder.tmpl b/gui/slick/interfaces/default/apiBuilder.tmpl index c3660035..44fb1233 100644 --- a/gui/slick/interfaces/default/apiBuilder.tmpl +++ b/gui/slick/interfaces/default/apiBuilder.tmpl @@ -8,7 +8,7 @@ sbRoot = "$sbRoot"; //--> - + + +
-
+
+ + + +
-
+
+
+
Bubble links:
+ + + + + + + + +
+
+
+ +
- -

Emby

+ +

Emby

Have a central media database with strong user management, e.g. for improved Kodi profile(s). Gain deep viewing and granular control on any device, e.g. replace Plex entirely, Emby + Kodi > Plex.

#if not hasattr($sickbeard, 'EMBY_UPDATE_LIBRARY')#Restart SickGear to reveal new options here#else# -
-
+ +
-
-
-
-
Click below to test.
- +
Click below to test
+ #end if -
+
+
- -

Kodi

-

Kodi (formerly known as XBMC) is an award-winning free and open source (GPL) software media player and entertainment hub.

+ +

Kodi

+

Kodi is a media player and entertainment hub.

-
-
+ +
-
-
-
-
-
-
-
-
-
-
-
Click below to test.
- - -
+
Click below to test
+ + +
-
- -
- -

XBMC

-

A free and open source cross-platform media center and home entertainment system software with a 10-foot user interface designed for the living-room TV.

-
-
-
- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - - -
-
- - -
-
- - -
-
Click below to test.
- - -
- -
- -
- -

Plex Media Server

-

Plex organizes all of your personal media, wherever you keep it, so you can enjoy it on any device

-

To send notifications to Plex Home Theater (PHT) clients, use the XBMC notifier with port 3005.

+ +

Plex Media Server

+

Plex organizes media, wherever stored, to be enjoyed anywhere.

+#if 'XBMC' in NotifierFactory().notifiers +

To send notifications to Plex Home Theater (PHT) clients, use the XBMC notifier with port 3005.

+#end if
-
-
+
-
-
+ +#if 'XBMC' in NotifierFactory().notifiers
- -

NMJ

-

The Networked Media Jukebox, or NMJ, is the official media jukebox interface made available for the Popcorn Hour 200-series.

+ +

XBMC

+

A media center and home entertainment system software with a 10-foot user interface designed for the living-room TV.

-
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Click below to test
+ + +
+
+
+#end if + + +
+
+ +

NMJ

+

The Networked Media Jukebox, is the official media jukebox interface for the Popcorn Hour 200 series.

+
+
+
+
-
+
-
-
-
-
-
Click below to test.
- - -
+
Click below to test
+ + +
+
-

NMJv2

-

The Networked Media Jukebox, or NMJv2, is the official media jukebox interface made available for the Popcorn Hour 300 & 400-series.

+

NMJv2

+

The Networked Media Jukebox, is the official media jukebox interface for Popcorn Hour 300/400 series.

-
-
+
-
Database location
-
-
-
-
Click below to test.
- - -
+
Click below to test
+ + +
@@ -608,30 +631,30 @@
- -

Synology

+ +

Syno Indexer

The Synology DiskStation NAS.

Synology Indexer is the daemon running on the Synology NAS to build its media database.

-
-
- -
+
+ +
@@ -639,194 +662,575 @@
- -

Synology Notifier

-

Synology Notifier is the notification system of Synology DSM

+ +

Syno Notifier

+

The Synology DSM notification system.

-
-
+
+ +
-
-
-
- -
+ +
- -

pyTivo

-

pyTivo is both an HMO and GoBack server. This notifier will load the completed downloads to your Tivo.

+ +

pyTivo

+

pyTivo is an HMO and GoBack server. This notifier will load the completed downloads to a Tivo.

-
-
+
-
-
-
- -
+ +
-
+ + +
+
+
+
Bubble links:
+ +#if 'PUSHALOT' in NotifierFactory().notifiers + +#end if + + + + + + +
+
+
+
- -

Growl

-

A cross-platform unobtrusive global notification system.

+ +

Boxcar2

+

Read messages where and when you want them.

-
-
+
-
-
-
- -
- - -
-
Click below to register and test Growl, this is required for Growl notifications to work.
- - -
+
Click below to test
+ + +
+ +
+
+ + +#if 'PUSHALOT' in NotifierFactory().notifiers +
+
+ +

Pushalot

+

Pushalot is a platform for receiving custom push notifications to connected devices running Windows Phone or Windows 8.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Click below to test
+ + +
+ +
+
+#end if + + +
+
+ +

Pushbullet

+

Pushbullet sends notifications to Android, iOS, and browsers.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Click below to test
+ + +
+ +
+
+ + +
+
+ +

Pushover

+

Pushover sends real-time notifications to Android and iOS devices.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Click below to test
+ + +
+ +
+
+ + +
+
+ +

Growl

+

Self-hosted private device to device notification system made for OS X, available on Windows. Snarl on Windows and Growl for Android.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Click below to register and test Growl, this is required for Growl notifications to work
+ + +
@@ -834,728 +1238,275 @@
- Prowl -

Prowl

+ Prowl +

Prowl

A Growl client for iOS.

-
-
+
-
-
-
-
-
-
Click below to test.
- - -
+
Click below to test
+ + +
-
-
- -

Libnotify

-

The standard desktop notification API for Linux/*nix systems. This notifier will only function if the pynotify module is installed (Ubuntu/Debian package python-notify).

-
-
-
- -
- -
-
- -
-
- -
-
- -
-
Click below to test.
- - -
- -
-
- - -
-
- -

Pushover

-

Pushover makes it easy to send real-time notifications to your Android and iOS devices.

-
-
-
- -
- -
-
- -
-
- -
-
- -
-
- - -
-
- - -
-
- - - -
-
- - -
-
- - -
-
Click below to test.
- - -
- -
-
- -
-
- -

Boxcar2

-

Read your messages where and when you want them!

-
-
-
- -
- -
-
- -
-
- -
-
- -
-
- - -
-
- - -
-
Click below to test.
- - -
- -
-
-
-

Notify My Android

-

Notify My Android is a Prowl-like Android App and API that offers an easy way to send notifications from your application directly to your Android device.

+

Notify My Android

+

Notify My Android is a Prowl-like Android app and API that sends notifications from applications directly to Android devices.

-
-
+
-
-
-
- -
-
-
Click below to test.
- - -
+
Click below to test
+ + +
+
- -

Pushalot

-

Pushalot is a platform for receiving custom push notifications to connected devices running Windows Phone or Windows 8.

+ +

Libnotify

+

Standard desktop notification API for Linux/*nix systems. Requires pynotify module (Ubuntu/Debian package python-notify).

-
-
+
-
-
-
-
- - -
-
Click below to test.
- - -
+
Click below to test
+ + +
-
- -
-
- -

Pushbullet

-

Pushbullet allows you to send push notifications to Android, iOS and supported browsers.

-
-
-
- -
- -
-
- -
-
- -
-
- -
-
- - -
-
- - -
-
Click below to test.
- - -
- -
-
- +
+ + + +
-
-
- -

Twitter

-

A social networking and microblogging service, enabling its users to send and read other users' messages called tweets.

+
+
+
Bubble links:
+ + + + + + +
-
-
- - -
+
-
-
- -
-
- -
-
- -
-
- - -
-
- - -
- -
Click below to test.
- - -
- - -
- - -
+
-

Trakt

-

Trakt can keep a record of what TV shows you are watching and recommend additional shows based on your show data.

+

Trakt

+

Trakt can recommend shows based on notifications. (Add show... Trakt cards/Recommend)

-
-
+
-
@@ -1573,27 +1524,27 @@ .. #end if #for $void, $account in $trakt_accounts.items() - $account.name#if $account.active then '' else '
(inactive)'# + $account.name#if $account.active then '' else '
(inactive)'# #end for #if not $root_dirs: - #set $root_dirs = [{'root_def': False, 'loc': 'all folders. Multiple parent folders will appear here.', 'b64': ''}] + #set $root_dirs = [{'root_def': False, 'loc': 'all folders. Multiple parent folders will appear here.', 'b64': ''}] #end if #for $root_info in $root_dirs: Update collection - #if not len($trakt_accounts) + #if not len($trakt_accounts) .. - #end if - #for $void, $account in $trakt_accounts.items() - #set $cur_selected = ('', ' checked="checked"')[$root_info['loc'] in $sickbeard.TRAKT_UPDATE_COLLECTION.get($account.account_id, '')] - #set $id_loc = "update_trakt_%s_%s" % ($account.account_id, $root_info['b64']) + #end if + #for $void, $account in $trakt_accounts.items() + #set $cur_selected = ('', ' checked="checked"')[$root_info['loc'] in $sickbeard.TRAKT_UPDATE_COLLECTION.get($account.account_id, '')] + #set $id_loc = "update-trakt-%s-%s" % ($account.account_id, $root_info['b64']) - + - #end for + #end for for #if $root_info['root_def'] then '*' else ''#$root_info['loc'] @@ -1605,10 +1556,10 @@
- -
+ +
+
- -

Email

-

Email notification settings.

+ +

Slack

+

Team, group, and direct communication.

-
-
+
-
-
-
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
Click below to test
+ + +
+
+
+ + +
+
+ +

Discordapp

+

Voice and text chat.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
Click below to test
+ + +
+
+
+ + +
+
+ +

Gitter

+

Gitter chat and networking platform.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+
-
+
+ +
+
Click below to test
+ + +
+
+
+ + +
+
+ +

Twitter

+

A social networking and microblogging service.

+
+
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
Click below to test
+ + +
+ +
+
+ + +
+
+ +

Email

+

Email notification settings.

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
SMTP server hostname - - and port + + and port
-
-
-
-
Click below to test.
- - -
+
Click below to test
+ + +
-

+

- - + @@ -43,118 +46,118 @@ #end if
-
+
-
+ -
- +
+ -
+
-
-

Subtitles Search

-

Settings that dictate how SickGear handles subtitles search results.

-
+
+

Subtitles Search

+

Settings that dictate how SickGear handles subtitles search results.

+
-
- - -
-
-
- -
-
- - - -
-
- - -
-
- - -
-

-
-
-
+
+ + +
+
+
+ +
+
+ + + +
+
+ + +
+
+ + +
+

+
+ +
-
+
-
-

Subtitle Plugins

-

Check off and drag the plugins into the order you want them to be used.

-

At least one plugin is required.

-

* Web-scraping plugin

-
+
+

Subtitle Plugins

+

Check off and drag the plugins into the order you want them to be used.

+

At least one plugin is required.

+

* Web-scraping plugin

+
-
-
    - #for $curService in $sickbeard.subtitles.sortedServiceList(): - #set $curName = $curService.id -
  • - - #set $provider_url = $curService.url - - $curService.name - - $curService.name.capitalize() - #if not $curService.api_based then "*" else ""# - -
  • - #end for -
- "/> - -

-
-
+
+
    +#for $curService in $sickbeard.subtitles.sortedServiceList(): + #set $curName = $curService.id +
  • + + #set $provider_url = $curService.url + + $curService.name + + $curService.name.capitalize() + #if not $curService.api_based#*#end if# + +
  • +#end for +
+ "> -

+

+
+
-
+

- -
+
+ + +
-#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl') \ No newline at end of file +#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl') diff --git a/gui/slick/interfaces/default/displayShow.tmpl b/gui/slick/interfaces/default/displayShow.tmpl index a3af1244..ff0e044d 100644 --- a/gui/slick/interfaces/default/displayShow.tmpl +++ b/gui/slick/interfaces/default/displayShow.tmpl @@ -17,7 +17,7 @@ #set global $page_body_attr = 'display-show" class="' + $css #set theme_suffix = ('', '-dark')['dark' == $sg_str('THEME_NAME', 'dark')] ## -#import os.path, os +#import os.path, os, re #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl') @@ -301,7 +301,7 @@ #set $dev = True #set $dev = None
-#set $is_master_settable = False +#set $is_master_settable = False | $unlock_master_id #for $src_id, $src_name in $sickbeard.indexerApi().all_indexers.iteritems() #set $is_master_settable |= ($dev or ($src_id != $show.indexer and $show.ids[$src_id].get('id', 0) > 0 and @@ -328,13 +328,23 @@ #set $data_link = '' #set $use_search_url = False #end if - $src_name + $src_name #end if $src_name - - #if $src_id != $show.indexer + + #set $current_showid = $show.ids.get($src_id, {'id': 0}).get('id') + + + #if $src_id == $show.indexer + + #else #end if - #else - #end if
diff --git a/gui/slick/interfaces/default/history.tmpl b/gui/slick/interfaces/default/history.tmpl index aa288c15..2ff7a49b 100644 --- a/gui/slick/interfaces/default/history.tmpl +++ b/gui/slick/interfaces/default/history.tmpl @@ -1,10 +1,9 @@ -#import sickbeard #import datetime #import re -#from sickbeard.common import * -#from sickbeard import sbdatetime -#from sickbeard import history -#from sickbeard import providers + +#import sickbeard +#from sickbeard import history, providers, sbdatetime +#from sickbeard.common import Quality, statusStrings, SNATCHED_ANY, SNATCHED_PROPER, DOWNLOADED, SUBTITLED, ARCHIVED, FAILED #from sickbeard.providers import generic <% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp# <% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp# @@ -58,8 +57,7 @@ }); \$('#limit').change(function(){ - url = '$sbRoot/history/?limit=' + \$(this).val() - window.location.href = url + window.location.href = '$sbRoot/history/?limit=' + \$(this).val() }); #set $fuzzydate = 'airdate' @@ -131,21 +129,21 @@ #set $curdatetime = $datetime.datetime.strptime(str($hItem['date']), $history.dateFormat)
$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)
$time.mktime($curdatetime.timetuple()) - $display_name#if 'proper' in $hItem['resource'].lower or 'repack' in $hItem['resource'].lower then ' Proper' else ''# - - #if SUBTITLED == $curStatus + $display_name#if $Quality.splitCompositeStatus($hItem['action'])[0] == $SNATCHED_PROPER then ' Proper' else ''# + + #if $SUBTITLED == $curStatus "> #end if $statusStrings[$curStatus] - #if DOWNLOADED == $curStatus + #if $DOWNLOADED == $curStatus #if '-1' != $hItem['provider'] $hItem['provider'] #end if #else #if 0 < $hItem['provider'] - #if $curStatus in [SNATCHED, FAILED] + #if $curStatus in $SNATCHED_ANY + [$FAILED] #set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider'])) #if None is not $provider $provider.name @@ -197,20 +195,20 @@ #for $action in reversed($hItem['actions']) #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action'])) #set $basename = $os.path.basename($action['resource']) - #if $curStatus in [SNATCHED, FAILED] + #if $curStatus in $SNATCHED_ANY + [$FAILED] #set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($action['provider'])) #if None is not $provider #set $prov_list += ['%s'\ - % (('', ' class="fail"')[FAILED == $curStatus], $sbRoot, $provider.image_name(), $provider.name, - ('%s%s' % ($order, 'th' if $order in [11, 12, 13] or str($order)[-1] not in $ordinal_indicators else $ordinal_indicators[str($order)[-1]]), 'Snatch failed')[FAILED == $curStatus], + % (('', ' class="fail"')[$FAILED == $curStatus], $sbRoot, $provider.image_name(), $provider.name, + ('%s%s' % ($order, 'th' if $order in [11, 12, 13] or str($order)[-1] not in $ordinal_indicators else $ordinal_indicators[str($order)[-1]]), 'Snatch failed')[$FAILED == $curStatus], $provider.name, $basename)] - #set $order += (0, 1)[SNATCHED == $curStatus] + #set $order += (0, 1)[$curStatus in $SNATCHED_ANY] #else #set $prov_list += ['missing provider'\ % $sbRoot] #end if #end if - #if $curStatus in [DOWNLOADED, ARCHIVED] + #if $curStatus in [$DOWNLOADED, $ARCHIVED] #set $match = $re.search('\-(\w+)\.\w{3}\Z', $basename) #set $non_scene_note = '' #if not $match @@ -251,7 +249,7 @@ #for $action in reversed($hItem['actions']) #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action'])) - #if SUBTITLED == $curStatus + #if $SUBTITLED == $curStatus $action['provider'] / diff --git a/gui/slick/interfaces/default/home.tmpl b/gui/slick/interfaces/default/home.tmpl index a5babd07..bb3abbb4 100644 --- a/gui/slick/interfaces/default/home.tmpl +++ b/gui/slick/interfaces/default/home.tmpl @@ -1,5 +1,6 @@ #import sickbeard #import datetime +#from sickbeard import WEB_ROOT, THEME_NAME #from sickbeard.common import * #from sickbeard import sbdatetime, network_timezones <% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp# @@ -11,6 +12,7 @@ #set global $topmenu = 'home' #set global $page_body_attr = 'show-list' #set fuzzydate = 'airdate' +#set sg_root = $getVar('sbRoot', WEB_ROOT) ## #import os.path #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl') @@ -28,7 +30,13 @@ fuzzydate: ".$fuzzydate", }; - + + +
+

$showlists[0][1]

@@ -38,18 +46,18 @@
Sort By: Sort Order: @@ -58,10 +66,10 @@ Layout: #end if @@ -74,6 +82,8 @@
## +#set $poster_id = 0 +#set $load_normal = 0 #for $curShowlist in $showlists #set $curListID = $curShowlist[0] #set $curListName = $curShowlist[1] @@ -98,7 +108,7 @@ #if None is $curLoadingShow.show ##
- +
Loading... ($curLoadingShow.show_name)
@@ -119,6 +129,7 @@ #set $download_stat_tip = '' #set $display_status = $curShow.status #set $display_name = (re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\1 \2', $curShow.name), $curShow.name)[$sg_var('SORT_ARTICLE')] + #set $poster_id += 1 #if None is not $display_status #if re.search(r'(?i)(?:new|returning)\s*series', $curShow.status) #set $display_status = 'Continuing' @@ -150,7 +161,7 @@ #set $download_stat = str($cur_downloaded) #set $download_stat_tip = 'Downloaded: ' + str($cur_downloaded) #if $cur_snatched > 0 - #set $download_stat = '%s+%s' % ($download_stat, $sbRoot, $cur_snatched) + #set $download_stat = '%s+%s' % ($download_stat, $sg_root, $cur_snatched) #set $download_stat_tip = download_stat_tip + ' ' + 'Snatched: ' + str($cur_snatched) #end if #set $download_stat = download_stat + ' / ' + str($cur_total) @@ -184,7 +195,14 @@
@@ -224,7 +242,7 @@ #if 'No Network' is not $img_text and 'nonetwork' in $network_images[$curShow.indexerid] $curShow.network #else - $img_text + $img_text #end if #end if @@ -267,7 +285,7 @@ - Add Show + Add Show @@ -284,7 +302,7 @@ #if None is $curLoadingShow.show Loading... ($curLoadingShow.show_name) #else - $curLoadingShow.show.name + $curLoadingShow.show.name #end if @@ -297,6 +315,8 @@ ## #set void = $myShowList.sort(lambda x, y: cmp(x.name, y.name)) ## + #set $poster_id = 0 + #set $load_normal = 0 #for $curShow in $myShowList ## #set $cur_airs_next = '' @@ -305,6 +325,7 @@ #set $cur_total = 0 #set $download_stat_tip = '' #set $display_name = (re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\1 \2', $curShow.name), $curShow.name)[$sg_var('SORT_ARTICLE')] + #set $poster_id += 1 ## #if $curShow.indexerid in $show_stat #set $cur_airs_next = $show_stat[$curShow.indexerid]['ep_airs_next'] @@ -329,7 +350,7 @@ #set $download_stat = str($cur_downloaded) #set $download_stat_tip = 'Downloaded: ' + str($cur_downloaded) #if $cur_snatched > 0 - #set $download_stat = '%s+%s' % ($download_stat, $sbRoot, $cur_snatched) + #set $download_stat = '%s+%s' % ($download_stat, $sg_root, $cur_snatched) #set $download_stat_tip = download_stat_tip + ' ' + 'Snatched: ' + str($cur_snatched) #end if #set $download_stat = download_stat + ' / ' + str($cur_total) @@ -361,25 +382,35 @@ #else if 'banner' == $layout $display_name #else if 'simple' == $layout - $display_name + $display_name #end if #if 'simple' != $layout #set $img_text = ($curShow.network, 'No Network')[None is $curShow.network] @@ -388,7 +419,7 @@ #if 'No Network' is not $img_text and 'nonetwork' in $network_images[$curShow.indexerid] $curShow.network #else - #echo '%s + #echo '%s $curShow.network #end if @@ -438,5 +469,7 @@ #end for ## - + + + #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_bottom.tmpl') diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl index 7fa29350..d4f7a12a 100644 --- a/gui/slick/interfaces/default/home_browseShows.tmpl +++ b/gui/slick/interfaces/default/home_browseShows.tmpl @@ -2,6 +2,7 @@ #import datetime #import re #import urllib +#from sickbeard import WEB_ROOT, THEME_NAME #from sickbeard.common import * #from sickbeard import sbdatetime #from sickbeard.helpers import anon_url @@ -13,11 +14,12 @@ #set global $sbPath='..' #set global $topmenu='home' #set global $page_body_attr = 'browse-list' +#set sg_root = $getVar('sbRoot', WEB_ROOT) ## #import os.path #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl') - + + + +
+ #if $varExists('header') #set $heading = ('header', $header) #else @@ -123,15 +210,35 @@ #set $mode = $kwargs and $kwargs.get('mode', '') #if $all_shows or ($kwargs and $kwargs.get('show_header'))
- View: - + #set $num_all = len($all_shows) #set $selected = ' class="selected"' - #if 'Trakt' == $browse_type + + + + + #if 'Ani' not in $browse_type + + #end if + + + + + + + + + + + + + + + + #if 'Ani' not in $browse_type + #end if - - - Sort By: - - - Sort Order: -

$browse_title

- #if $kwargs and $kwargs.get('oldest'): + #if $kwargs and $kwargs.get('oldest')
First aired from $kwargs['oldest'] until $kwargs['newest']
@@ -214,7 +318,10 @@
#if $all_shows - #for $this_show in $all_shows: + #set $poster_id = 0 + #for $this_show in $all_shows + #set $poster_id += 1 + #set $title_html = $this_show['title'].replace('"', '"').replace("'", ''') #if 'newseasons' == $mode #set $overview = '%s: %s' % ( @@ -224,7 +331,15 @@ #set $overview = $this_show['overview'] #end if -
+ #set $known = 'not' + #set $show_id = $this_show['show_id'] + #if ':' in $show_id + #set $known = '' + #set $show_id = $show_id[2:] + #end if + #set $hide = ('', 'hide ')[$show_id in $sickbeard.BROWSELIST_HIDDEN] + +
#if $kwargs and 'newseasons' == $mode#Air#else#First air#end if##echo ('s', 'ed')[$this_show['when_past']]#: $this_show['premiered_str'] #if $this_show.get('ended_str')# - Ended: $this_show['ended_str']#end if#

Click for more at $browse_type"> - #if 'poster' in $this_show['images']: + #if 'poster' in $this_show['images'] #set $image = $this_show['images']['poster']['thumb'] - - #else: + + + #else   #end if
@@ -245,20 +361,22 @@
#echo ((re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\1 \2', $this_show['title']), $this_show['title'])[$sg_var('SORT_ARTICLE')], ' ')['' == $this_show['title']]#
- + #if 'Ani' not in $browse_type + + #end if

$this_show['rating']%$this_show['votes'] votes

- #if 'url_tvdb' in $this_show and $this_show['url_tvdb']: + #if 'url_tvdb' in $this_show and $this_show['url_tvdb'] - tvdb + tvdb #end if
- #if ':' in $this_show['show_id']: + #if ':' in $this_show['show_id']

In library

#else - Add Show + Add Show #end if
@@ -270,15 +388,15 @@
#end for
- #if $kwargs and $kwargs.get('footnote'): -
+ #if $kwargs and $kwargs.get('footnote') +
$kwargs['footnote']
#end if #else

- #if $kwargs and $kwargs.get('error_msg'): + #if $kwargs and $kwargs.get('error_msg') $kwargs['error_msg'] #else $browse_type API did not return results, this can happen from time to time. @@ -295,4 +413,6 @@ window.setInterval('location.reload(true)', 600000); // Refresh every 10 minutes //--> + + #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_bottom.tmpl') diff --git a/gui/slick/interfaces/default/home_massAddTable.tmpl b/gui/slick/interfaces/default/home_massAddTable.tmpl index 0fb92ac9..2bec4ca1 100644 --- a/gui/slick/interfaces/default/home_massAddTable.tmpl +++ b/gui/slick/interfaces/default/home_massAddTable.tmpl @@ -1,12 +1,15 @@ #import re #import sickbeard #from sickbeard.helpers import anon_url +<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp# +<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp# +#set $state_checked = ('', ' checked=checked')[any([sg_var('IMPORT_DEFAULT_CHECKED_SHOWS')])] @@ -41,7 +44,7 @@ + #set $nfo, $nfo_img = (('No', '-no'), ('Yes', ''))[int($ep['hasnfo'])] #set $tbn, $tbn_img = (('No', '-no'), ('Yes', ''))[int($ep['hastbn'])] - + #if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower()) #set $epLoc = $epLoc[len($show._location)+1:] @@ -76,14 +76,15 @@ #else value="" #end if - style="padding:0; text-align:center; max-width:60px" /> + style="padding:0; text-align:center; max-width:60px"> #end if #slurp + #else #end if diff --git a/gui/slick/interfaces/default/inc_top.tmpl b/gui/slick/interfaces/default/inc_top.tmpl index 44448072..603c39cf 100644 --- a/gui/slick/interfaces/default/inc_top.tmpl +++ b/gui/slick/interfaces/default/inc_top.tmpl @@ -39,28 +39,29 @@ - + + - - + + + - + - - - - + + + + - - - + + - - + + @@ -74,7 +75,7 @@ @@ -93,6 +94,13 @@ #set $parts = $body_attr.split('class="') #set $body_attr = ('class="%s '.join($parts), $parts[0] + ' class="%s"')[1 == len($parts)] % {0: '', 1: 'pro', 2: 'pro ii'}.get(getattr($sickbeard, 'DISPLAY_SHOW_VIEWMODE', 0)) #end if + +#set $classes = ' '.join(([], ['shows-not-found'])[any([$getVar('log_num_not_found_shows_all', 0)])] \ + + ([], [($getVar('log_num_not_found_shows', 0) * 'n')[0:4]])[any([$getVar('log_num_not_found_shows', 0)])]) +#if any($classes) + #set $body_attr = $body_attr.rstrip('"') + (' class="%s"', ' %s"')['class=' in $body_attr] % $classes +#end if +
- + Parent\show folder Show name
(tvshow.nfo)
- +
- #if $UNAIRED != int($ep['status']) and not $never_aired + #if $UNAIRED != int($ep['status']) #end if $nfo
- $tbn
$nfo
+ $tbn
- ' %\ - ('None opacity40', ('" id="plot_info_%s_%s_%s' % ($show.indexerid, $ep['season'], $ep['episode'])))[None is not $ep['description'] and '' != $ep['description']]# - #if not $ep['name'] or 'TBA' == $ep['name']#TBA#else#$ep['name']#end if# + + #set $cls = (' class="tba grey-text"', '')['good' == $Overview.overviewStrings[$ep_cats[$ep_str]]] + #if not $ep['name'] or 'TBA' == $ep['name']#TBA#else#$ep['name']#end if# @@ -95,7 +96,7 @@ #if $ep['subtitles'] #for $sub_lang in subliminal.language.language_list($ep['subtitles'].split(',')) #if '' != sub_lang.alpha2 - ${sub_lang} + ${sub_lang} #end if #end for #end if @@ -104,21 +105,21 @@ #slurp #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($ep['status'])) #if Quality.NONE != $curQuality - #if SUBTITLED == $curStatus##else#$statusStrings[$curStatus].replace('Downloaded', '')#end if# $Quality.qualityStrings[$curQuality]#if $SUBTITLED == $curStatus##else#$statusStrings[$curStatus].replace('Downloaded', '')#end if# $Quality.qualityStrings[$curQuality]$statusStrings[$curStatus]
#set $row = 0 - #for $cur_item in $queueLength['backlog']: + #for $cur_item in $queue_length['backlog']: #set $search_type = 'On Demand' #if $cur_item['standard_backlog']: #if $cur_item['forced']: @@ -90,17 +90,17 @@ Backlog: $len($queueLength['backlog']) item$sickbeard.helpers.maybe_plural($l #else -
+
#end if -
-Manual: $len($queueLength['manual']) item$sickbeard.helpers.maybe_plural($len($queueLength['manual'])) -#if $queueLength['manual'] -
+
+Manual: $len($queue_length['manual']) item$sickbeard.helpers.maybe_plural($len($queue_length['manual'])) +#if $queue_length['manual'] +
#set $row = 0 - #for $cur_item in $queueLength['manual']: + #for $cur_item in $queue_length['manual']: #else -
+
#end if -
-Failed: $len($queueLength['failed']) item$sickbeard.helpers.maybe_plural($len($queueLength['failed'])) -#if $queueLength['failed'] -
+
+Failed: $len($queue_length['failed']) item$sickbeard.helpers.maybe_plural($len($queue_length['failed'])) +#if $queue_length['failed'] +
#set $row = 0 - #for $cur_item in $queueLength['failed']: + #for $cur_item in $queue_length['failed']: #else -
+
#end if

diff --git a/gui/slick/interfaces/default/manage_showProcesses.tmpl b/gui/slick/interfaces/default/manage_showProcesses.tmpl index c28b1e82..dbc01bf7 100644 --- a/gui/slick/interfaces/default/manage_showProcesses.tmpl +++ b/gui/slick/interfaces/default/manage_showProcesses.tmpl @@ -1,5 +1,5 @@ #import sickbeard -#from sickbeard.helpers import findCertainShow +#from sickbeard.helpers import findCertainShow, maybe_plural ## #set global $title = 'Show Processes' #set global $header = 'Show Processes' @@ -17,135 +17,222 @@

$title

#end if -
-

Daily Show Update:

- Force -#if not $ShowUpdateRunning: - Not in progress
+
+

Daily show update:

+ Force +#if not $show_update_running: + Not in progress
#else: - Currently running
+ Currently running
#end if -
-

Show Queue:

-
-#if $queueLength['add'] or $queueLength['update'] or $queueLength['refresh'] or $queueLength['rename'] or $queueLength['subtitle'] -
-#end if -
-Add: $len($queueLength['add']) show$sickbeard.helpers.maybe_plural($len($queueLength['add'])) -#if $queueLength['add'] -
- - - - #set $row = 0 - #for $cur_show in $queueLength['add']: - #set $show_name = str($cur_show['name']) - - - - - #end for - - -#else -
-#end if -
-Update (Forced / Forced Web): $len($queueLength['update']) ($len($queueLength['forceupdate']) / $len($queueLength['forceupdateweb'])) show$sickbeard.helpers.maybe_plural($len($queueLength['update'])) -#if $queueLength['update'] -
- - - - #set $row = 0 - #for $cur_show in $queueLength['update']: - #set $show = $findCertainShow($showList, $cur_show['indexerid']) - #set $show_name = $show.name if $show else str($cur_show['name']) - - - - - #end for - - -#else -
-#end if -
-Refresh: $len($queueLength['refresh']) show$sickbeard.helpers.maybe_plural($len($queueLength['refresh'])) -#if $queueLength['refresh'] -
- - - - #set $row = 0 - #for $cur_show in $queueLength['refresh']: - #set $show = $findCertainShow($showList, $cur_show['indexerid']) - #set $show_name = $show.name if $show else str($cur_show['name']) - - - - - #end for - - -#else -
-#end if -
-Rename: $len($queueLength['rename']) show$sickbeard.helpers.maybe_plural($len($queueLength['rename'])) -#if $queueLength['rename'] -
- - - - - #set $row = 0 - #for $cur_show in $queueLength['rename']: - #set $show = $findCertainShow($showList, $cur_show['indexerid']) - #set $show_name = $show.name if $show else str($cur_show['name']) - - - - - #end for - - -#else -
-#end if -#if $sickbeard.USE_SUBTITLES -
- Subtitle: $len($queueLength['subtitle']) show$sickbeard.helpers.maybe_plural($len($queueLength['subtitle'])) - #if $queueLength['subtitle'] -
- - +
+#if $not_found_shows + #set $num_errors = $len($not_found_shows) + #set $err_class = ('', ' errors ' + ($num_errors * 'n')[0:4])[any([$num_errors])] +

$num_errors Show$maybe_plural($num_errors) with abandoned master ID$maybe_plural($num_errors):

+

List of show(s) with changed ID at the TV info source. Click show name to get new ID, so that episode info updates may continue

+
+ + + + + + + + - #set $row = 0 - #for $cur_show in $queueLength['subtitle']: - #set $show = $findCertainShow($showList, $cur_show['indexerid']) - #set $show_name = $show.name if $show else str($cur_show['name']) + #set $row = 0 + #for $cur_show in $not_found_shows: - + + + + #end for + + + + + + + +
Show nameLast foundIgnore Warn
+ + $cur_show['show_name'] + + $cur_show['last_success'] + +
Note: Ignored shows will still not get updates unless edited + +
+#end if +#if $defunct_indexer + +

Shows from defunct TV info sources:

+
+ + + + + + + + #set $row = 0 + #for $cur_show in $defunct_indexer: + + + + #end for + +
Show name
+ $cur_show['show_name'] +
+#end if + +

Show queue:

+#if $queue_length['add'] or $queue_length['update'] or $queue_length['refresh'] or $queue_length['rename'] or $queue_length['subtitle'] +
+
+#end if +
+ Add: $len($queue_length['add']) show$sickbeard.helpers.maybe_plural($len($queue_length['add'])) +#if $queue_length['add'] +
+ + + + + + + + + #set $row = 0 + #for $cur_show in $queue_length['add']: + #set $show_name = str($cur_show['name']) + + + + + #end for + + +#else +
+#end if +
+ Update (Forced / Forced Web): $len($queue_length['update']) ($len($queue_length['forceupdate']) / $len($queue_length['forceupdateweb'])) show$sickbeard.helpers.maybe_plural($len($queue_length['update'])) +#if $queue_length['update'] +
+ + + + + + + + + #set $row = 0 + #for $cur_show in $queue_length['update']: + #set $show = $findCertainShow($show_list, $cur_show['indexerid']) + #set $show_name = $show.name if $show else str($cur_show['name']) + + - + - #end for + #end for + + +#else +
+#end if +
+ Refresh: $len($queue_length['refresh']) show$sickbeard.helpers.maybe_plural($len($queue_length['refresh'])) +#if $queue_length['refresh'] +
+ + + + + + + + + #set $row = 0 + #for $cur_show in $queue_length['refresh']: + #set $show = $findCertainShow($show_list, $cur_show['indexerid']) + #set $show_name = $show.name if $show else str($cur_show['name']) + + + + + #end for + + +#else +
+#end if +
+ Rename: $len($queue_length['rename']) show$sickbeard.helpers.maybe_plural($len($queue_length['rename'])) +#if $queue_length['rename'] +
+ + + + + + + + + + #set $row = 0 + #for $cur_show in $queue_length['rename']: + #set $show = $findCertainShow($show_list, $cur_show['indexerid']) + #set $show_name = $show.name if $show else str($cur_show['name']) + + + + + #end for + + +#else +
+#end if +#if $sickbeard.USE_SUBTITLES +
+ Subtitle: $len($queue_length['subtitle']) show$sickbeard.helpers.maybe_plural($len($queue_length['subtitle'])) + #if $queue_length['subtitle'] +
+ + + + + + + + + #set $row = 0 + #for $cur_show in $queue_length['subtitle']: + #set $show = $findCertainShow($show_list, $cur_show['indexerid']) + #set $show_name = $show.name if $show else str($cur_show['name']) + + + + + #end for #else -
+
#end if #end if +
-
-#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl') \ No newline at end of file +#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl') diff --git a/gui/slick/interfaces/default/restart.tmpl b/gui/slick/interfaces/default/restart.tmpl index f7cfa9ab..0927d536 100644 --- a/gui/slick/interfaces/default/restart.tmpl +++ b/gui/slick/interfaces/default/restart.tmpl @@ -15,7 +15,7 @@ -SickGear - Restarting +SickGear - #echo ('Restart', 'Shutdown')[$do_shutdown]# @@ -35,7 +35,7 @@ - + ' - for p in paths) + js = self.render_linked_js(js_files) sloc = html.rindex(b'') html = html[:sloc] + utf8(js) + b'\n' + html[sloc:] if js_embed: - js = b'' + js = self.render_embed_js(js_embed) sloc = html.rindex(b'') html = html[:sloc] + js + b'\n' + html[sloc:] if css_files: - paths = [] - unique_paths = set() - for path in css_files: - if not is_absolute(path): - path = self.static_url(path) - if path not in unique_paths: - paths.append(path) - unique_paths.add(path) - css = ''.join('' - for p in paths) + css = self.render_linked_css(css_files) hloc = html.index(b'') html = html[:hloc] + utf8(css) + b'\n' + html[hloc:] if css_embed: - css = b'' + css = self.render_embed_css(css_embed) hloc = html.index(b'') html = html[:hloc] + css + b'\n' + html[hloc:] if html_heads: @@ -800,6 +781,64 @@ class RequestHandler(object): html = html[:hloc] + b''.join(html_bodies) + b'\n' + html[hloc:] self.finish(html) + def render_linked_js(self, js_files): + """Default method used to render the final js links for the + rendered webpage. + + Override this method in a sub-classed controller to change the output. + """ + paths = [] + unique_paths = set() + + for path in js_files: + if not is_absolute(path): + path = self.static_url(path) + if path not in unique_paths: + paths.append(path) + unique_paths.add(path) + + return ''.join('' + for p in paths) + + def render_embed_js(self, js_embed): + """Default method used to render the final embedded js for the + rendered webpage. + + Override this method in a sub-classed controller to change the output. + """ + return b'' + + def render_linked_css(self, css_files): + """Default method used to render the final css links for the + rendered webpage. + + Override this method in a sub-classed controller to change the output. + """ + paths = [] + unique_paths = set() + + for path in css_files: + if not is_absolute(path): + path = self.static_url(path) + if path not in unique_paths: + paths.append(path) + unique_paths.add(path) + + return ''.join('' + for p in paths) + + def render_embed_css(self, css_embed): + """Default method used to render the final embedded css for the + rendered webpage. + + Override this method in a sub-classed controller to change the output. + """ + return b'' + def render_string(self, template_name, **kwargs): """Generate the given template with the given arguments. @@ -954,6 +993,9 @@ class RequestHandler(object): self._log() self._finished = True self.on_finish() + self._break_cycles() + + def _break_cycles(self): # Break up a reference cycle between this handler and the # _ui_module closures to allow for faster GC on CPython. self.ui = None @@ -1667,9 +1709,8 @@ def stream_request_body(cls): * The regular HTTP method (``post``, ``put``, etc) will be called after the entire body has been read. - There is a subtle interaction between ``data_received`` and asynchronous - ``prepare``: The first call to ``data_received`` may occur at any point - after the call to ``prepare`` has returned *or yielded*. + See the `file receiver demo `_ + for example usage. """ if not issubclass(cls, RequestHandler): raise TypeError("expected subclass of RequestHandler, got %r", cls) @@ -1727,7 +1768,38 @@ def addslash(method): return wrapper -class Application(httputil.HTTPServerConnectionDelegate): +class _ApplicationRouter(ReversibleRuleRouter): + """Routing implementation used internally by `Application`. + + Provides a binding between `Application` and `RequestHandler`. + This implementation extends `~.routing.ReversibleRuleRouter` in a couple of ways: + * it allows to use `RequestHandler` subclasses as `~.routing.Rule` target and + * it allows to use a list/tuple of rules as `~.routing.Rule` target. + ``process_rule`` implementation will substitute this list with an appropriate + `_ApplicationRouter` instance. + """ + + def __init__(self, application, rules=None): + assert isinstance(application, Application) + self.application = application + super(_ApplicationRouter, self).__init__(rules) + + def process_rule(self, rule): + rule = super(_ApplicationRouter, self).process_rule(rule) + + if isinstance(rule.target, (list, tuple)): + rule.target = _ApplicationRouter(self.application, rule.target) + + return rule + + def get_target_delegate(self, target, request, **target_params): + if isclass(target) and issubclass(target, RequestHandler): + return self.application.get_handler_delegate(request, target, **target_params) + + return super(_ApplicationRouter, self).get_target_delegate(target, request, **target_params) + + +class Application(ReversibleRouter): """A collection of request handlers that make up a web application. Instances of this class are callable and can be passed directly to @@ -1740,20 +1812,35 @@ class Application(httputil.HTTPServerConnectionDelegate): http_server.listen(8080) ioloop.IOLoop.current().start() - The constructor for this class takes in a list of `URLSpec` objects - or (regexp, request_class) tuples. When we receive requests, we - iterate over the list in order and instantiate an instance of the - first request class whose regexp matches the request path. - The request class can be specified as either a class object or a - (fully-qualified) name. + The constructor for this class takes in a list of `~.routing.Rule` + objects or tuples of values corresponding to the arguments of + `~.routing.Rule` constructor: ``(matcher, target, [target_kwargs], [name])``, + the values in square brackets being optional. The default matcher is + `~.routing.PathMatches`, so ``(regexp, target)`` tuples can also be used + instead of ``(PathMatches(regexp), target)``. - Each tuple can contain additional elements, which correspond to the - arguments to the `URLSpec` constructor. (Prior to Tornado 3.2, - only tuples of two or three elements were allowed). + A common routing target is a `RequestHandler` subclass, but you can also + use lists of rules as a target, which create a nested routing configuration:: - A dictionary may be passed as the third element of the tuple, - which will be used as keyword arguments to the handler's - constructor and `~RequestHandler.initialize` method. This pattern + application = web.Application([ + (HostMatches("example.com"), [ + (r"/", MainPageHandler), + (r"/feed", FeedHandler), + ]), + ]) + + In addition to this you can use nested `~.routing.Router` instances, + `~.httputil.HTTPMessageDelegate` subclasses and callables as routing targets + (see `~.routing` module docs for more information). + + When we receive requests, we iterate over the list in order and + instantiate an instance of the first request class whose regexp + matches the request path. The request class can be specified as + either a class object or a (fully-qualified) name. + + A dictionary may be passed as the third element (``target_kwargs``) + of the tuple, which will be used as keyword arguments to the handler's + constructor and `~RequestHandler.initialize` method. This pattern is used for the `StaticFileHandler` in this example (note that a `StaticFileHandler` can be installed automatically with the static_path setting described below):: @@ -1769,6 +1856,9 @@ class Application(httputil.HTTPServerConnectionDelegate): (r"/article/([0-9]+)", ArticleHandler), ]) + If there's no match for the current request's host, then ``default_host`` + parameter value is matched against host regular expressions. + You can serve static files by sending the ``static_path`` setting as a keyword argument. We will serve those files from the ``/static/`` URI (this is configurable with the @@ -1777,8 +1867,10 @@ class Application(httputil.HTTPServerConnectionDelegate): `StaticFileHandler` can be specified with the ``static_handler_class`` setting. + .. versionchanged:: 4.5 + Integration with the new `tornado.routing` module. """ - def __init__(self, handlers=None, default_host="", transforms=None, + def __init__(self, handlers=None, default_host=None, transforms=None, **settings): if transforms is None: self.transforms = [] @@ -1786,8 +1878,6 @@ class Application(httputil.HTTPServerConnectionDelegate): self.transforms.append(GZipContentEncoding) else: self.transforms = transforms - self.handlers = [] - self.named_handlers = {} self.default_host = default_host self.settings = settings self.ui_modules = {'linkify': _linkify, @@ -1810,8 +1900,6 @@ class Application(httputil.HTTPServerConnectionDelegate): r"/(favicon\.ico)", r"/(robots\.txt)"]: handlers.insert(0, (pattern, static_handler_class, static_handler_args)) - if handlers: - self.add_handlers(".*$", handlers) if self.settings.get('debug'): self.settings.setdefault('autoreload', True) @@ -1819,6 +1907,11 @@ class Application(httputil.HTTPServerConnectionDelegate): self.settings.setdefault('static_hash_cache', False) self.settings.setdefault('serve_traceback', True) + self.wildcard_router = _ApplicationRouter(self, handlers) + self.default_router = _ApplicationRouter(self, [ + Rule(AnyMatches(), self.wildcard_router) + ]) + # Automatically reload modified modules if self.settings.get('autoreload'): from tornado import autoreload @@ -1856,47 +1949,20 @@ class Application(httputil.HTTPServerConnectionDelegate): Host patterns are processed sequentially in the order they were added. All matching patterns will be considered. """ - if not host_pattern.endswith("$"): - host_pattern += "$" - handlers = [] - # The handlers with the wildcard host_pattern are a special - # case - they're added in the constructor but should have lower - # precedence than the more-precise handlers added later. - # If a wildcard handler group exists, it should always be last - # in the list, so insert new groups just before it. - if self.handlers and self.handlers[-1][0].pattern == '.*$': - self.handlers.insert(-1, (re.compile(host_pattern), handlers)) - else: - self.handlers.append((re.compile(host_pattern), handlers)) + host_matcher = HostMatches(host_pattern) + rule = Rule(host_matcher, _ApplicationRouter(self, host_handlers)) - for spec in host_handlers: - if isinstance(spec, (tuple, list)): - assert len(spec) in (2, 3, 4) - spec = URLSpec(*spec) - handlers.append(spec) - if spec.name: - if spec.name in self.named_handlers: - app_log.warning( - "Multiple handlers named %s; replacing previous value", - spec.name) - self.named_handlers[spec.name] = spec + self.default_router.rules.insert(-1, rule) + + if self.default_host is not None: + self.wildcard_router.add_rules([( + DefaultHostMatches(self, host_matcher.host_pattern), + host_handlers + )]) def add_transform(self, transform_class): self.transforms.append(transform_class) - def _get_host_handlers(self, request): - host = split_host_and_port(request.host.lower())[0] - matches = [] - for pattern, handlers in self.handlers: - if pattern.match(host): - matches.extend(handlers) - # Look for default host if not behind load balancer (for debugging) - if not matches and "X-Real-Ip" not in request.headers: - for pattern, handlers in self.handlers: - if pattern.match(self.default_host): - matches.extend(handlers) - return matches or None - def _load_ui_methods(self, methods): if isinstance(methods, types.ModuleType): self._load_ui_methods(dict((n, getattr(methods, n)) @@ -1926,16 +1992,40 @@ class Application(httputil.HTTPServerConnectionDelegate): except TypeError: pass - def start_request(self, server_conn, request_conn): - # Modern HTTPServer interface - return _RequestDispatcher(self, request_conn) - def __call__(self, request): # Legacy HTTPServer interface - dispatcher = _RequestDispatcher(self, None) - dispatcher.set_request(request) + dispatcher = self.find_handler(request) return dispatcher.execute() + def find_handler(self, request, **kwargs): + route = self.default_router.find_handler(request) + if route is not None: + return route + + if self.settings.get('default_handler_class'): + return self.get_handler_delegate( + request, + self.settings['default_handler_class'], + self.settings.get('default_handler_args', {})) + + return self.get_handler_delegate( + request, ErrorHandler, {'status_code': 404}) + + def get_handler_delegate(self, request, target_class, target_kwargs=None, + path_args=None, path_kwargs=None): + """Returns `~.httputil.HTTPMessageDelegate` that can serve a request + for application and `RequestHandler` subclass. + + :arg httputil.HTTPServerRequest request: current HTTP request. + :arg RequestHandler target_class: a `RequestHandler` class. + :arg dict target_kwargs: keyword arguments for ``target_class`` constructor. + :arg list path_args: positional arguments for ``target_class`` HTTP method that + will be executed while handling a request (``get``, ``post`` or any other). + :arg dict path_kwargs: keyword arguments for ``target_class`` HTTP method. + """ + return _HandlerDelegate( + self, request, target_class, target_kwargs, path_args, path_kwargs) + def reverse_url(self, name, *args): """Returns a URL path for handler named ``name`` @@ -1945,8 +2035,10 @@ class Application(httputil.HTTPServerConnectionDelegate): They will be converted to strings if necessary, encoded as utf8, and url-escaped. """ - if name in self.named_handlers: - return self.named_handlers[name].reverse(*args) + reversed_url = self.default_router.reverse_url(name, *args) + if reversed_url is not None: + return reversed_url + raise KeyError("%s not found in named urls" % name) def log_request(self, handler): @@ -1971,67 +2063,24 @@ class Application(httputil.HTTPServerConnectionDelegate): handler._request_summary(), request_time) -class _RequestDispatcher(httputil.HTTPMessageDelegate): - def __init__(self, application, connection): +class _HandlerDelegate(httputil.HTTPMessageDelegate): + def __init__(self, application, request, handler_class, handler_kwargs, + path_args, path_kwargs): self.application = application - self.connection = connection - self.request = None + self.connection = request.connection + self.request = request + self.handler_class = handler_class + self.handler_kwargs = handler_kwargs or {} + self.path_args = path_args or [] + self.path_kwargs = path_kwargs or {} self.chunks = [] - self.handler_class = None - self.handler_kwargs = None - self.path_args = [] - self.path_kwargs = {} + self.stream_request_body = _has_stream_request_body(self.handler_class) def headers_received(self, start_line, headers): - self.set_request(httputil.HTTPServerRequest( - connection=self.connection, start_line=start_line, - headers=headers)) if self.stream_request_body: self.request.body = Future() return self.execute() - def set_request(self, request): - self.request = request - self._find_handler() - self.stream_request_body = _has_stream_request_body(self.handler_class) - - def _find_handler(self): - # Identify the handler to use as soon as we have the request. - # Save url path arguments for later. - app = self.application - handlers = app._get_host_handlers(self.request) - if not handlers: - self.handler_class = RedirectHandler - self.handler_kwargs = dict(url="%s://%s/" - % (self.request.protocol, - app.default_host)) - return - for spec in handlers: - match = spec.regex.match(self.request.path) - if match: - self.handler_class = spec.handler_class - self.handler_kwargs = spec.kwargs - if spec.regex.groups: - # Pass matched groups to the handler. Since - # match.groups() includes both named and - # unnamed groups, we want to use either groups - # or groupdict but not both. - if spec.regex.groupindex: - self.path_kwargs = dict( - (str(k), _unquote_or_none(v)) - for (k, v) in match.groupdict().items()) - else: - self.path_args = [_unquote_or_none(s) - for s in match.groups()] - return - if app.settings.get('default_handler_class'): - self.handler_class = app.settings['default_handler_class'] - self.handler_kwargs = app.settings.get( - 'default_handler_args', {}) - else: - self.handler_class = ErrorHandler - self.handler_kwargs = dict(status_code=404) - def data_received(self, data): if self.stream_request_body: return self.handler.data_received(data) @@ -2188,13 +2237,32 @@ class RedirectHandler(RequestHandler): application = web.Application([ (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}), ]) + + `RedirectHandler` supports regular expression substitutions. E.g., to + swap the first and second parts of a path while preserving the remainder:: + + application = web.Application([ + (r"/(.*?)/(.*?)/(.*)", web.RedirectHandler, {"url": "/{1}/{0}/{2}"}), + ]) + + The final URL is formatted with `str.format` and the substrings that match + the capturing groups. In the above example, a request to "/a/b/c" would be + formatted like:: + + str.format("/{1}/{0}/{2}", "a", "b", "c") # -> "/b/a/c" + + Use Python's :ref:`format string syntax ` to customize how + values are substituted. + + .. versionchanged:: 4.5 + Added support for substitutions into the destination URL. """ def initialize(self, url, permanent=True): self._url = url self._permanent = permanent - def get(self): - self.redirect(self._url, permanent=self._permanent) + def get(self, *args): + self.redirect(self._url.format(*args), permanent=self._permanent) class StaticFileHandler(RequestHandler): @@ -2990,99 +3058,6 @@ class _UIModuleNamespace(object): raise AttributeError(str(e)) -class URLSpec(object): - """Specifies mappings between URLs and handlers.""" - def __init__(self, pattern, handler, kwargs=None, name=None): - """Parameters: - - * ``pattern``: Regular expression to be matched. Any capturing - groups in the regex will be passed in to the handler's - get/post/etc methods as arguments (by keyword if named, by - position if unnamed. Named and unnamed capturing groups may - may not be mixed in the same rule). - - * ``handler``: `RequestHandler` subclass to be invoked. - - * ``kwargs`` (optional): A dictionary of additional arguments - to be passed to the handler's constructor. - - * ``name`` (optional): A name for this handler. Used by - `Application.reverse_url`. - - """ - if not pattern.endswith('$'): - pattern += '$' - self.regex = re.compile(pattern) - assert len(self.regex.groupindex) in (0, self.regex.groups), \ - ("groups in url regexes must either be all named or all " - "positional: %r" % self.regex.pattern) - - if isinstance(handler, str): - # import the Module and instantiate the class - # Must be a fully qualified name (module.ClassName) - handler = import_object(handler) - - self.handler_class = handler - self.kwargs = kwargs or {} - self.name = name - self._path, self._group_count = self._find_groups() - - def __repr__(self): - return '%s(%r, %s, kwargs=%r, name=%r)' % \ - (self.__class__.__name__, self.regex.pattern, - self.handler_class, self.kwargs, self.name) - - def _find_groups(self): - """Returns a tuple (reverse string, group count) for a url. - - For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method - would return ('/%s/%s/', 2). - """ - pattern = self.regex.pattern - if pattern.startswith('^'): - pattern = pattern[1:] - if pattern.endswith('$'): - pattern = pattern[:-1] - - if self.regex.groups != pattern.count('('): - # The pattern is too complicated for our simplistic matching, - # so we can't support reversing it. - return (None, None) - - pieces = [] - for fragment in pattern.split('('): - if ')' in fragment: - paren_loc = fragment.index(')') - if paren_loc >= 0: - pieces.append('%s' + fragment[paren_loc + 1:]) - else: - try: - unescaped_fragment = re_unescape(fragment) - except ValueError as exc: - # If we can't unescape part of it, we can't - # reverse this url. - return (None, None) - pieces.append(unescaped_fragment) - - return (''.join(pieces), self.regex.groups) - - def reverse(self, *args): - if self._path is None: - raise ValueError("Cannot reverse url regex " + self.regex.pattern) - assert len(args) == self._group_count, "required number of arguments "\ - "not found" - if not len(args): - return self._path - converted_args = [] - for a in args: - if not isinstance(a, (unicode_type, bytes)): - a = str(a) - converted_args.append(escape.url_escape(utf8(a), plus=False)) - return self._path % tuple(converted_args) - -url = URLSpec - - if hasattr(hmac, 'compare_digest'): # python 3.3 _time_independent_equals = hmac.compare_digest else: @@ -3147,6 +3122,7 @@ def create_signed_value(secret, name, value, version=None, clock=None, else: raise ValueError("Unsupported version %d" % version) + # A leading version number in decimal # with no leading zeros, followed by a pipe. _signed_value_version_re = re.compile(br"^([1-9][0-9]*)\|(.*)$") @@ -3305,13 +3281,5 @@ def _create_signature_v2(secret, s): return utf8(hash.hexdigest()) -def _unquote_or_none(s): - """None-safe wrapper around url_unescape to handle unmatched optional - groups correctly. - - Note that args are passed as bytes so the handler can decide what - encoding to use. - """ - if s is None: - return s - return escape.url_unescape(s, encoding=None, plus=False) +def is_absolute(path): + return any(path.startswith(x) for x in ["/", "http:", "https:"]) diff --git a/lib/tornado/websocket.py b/lib/tornado/websocket.py index 6e1220b3..69437ee4 100644 --- a/lib/tornado/websocket.py +++ b/lib/tornado/websocket.py @@ -16,7 +16,7 @@ the protocol (known as "draft 76") and are not compatible with this module. Removed support for the draft 76 protocol version. """ -from __future__ import absolute_import, division, print_function, with_statement +from __future__ import absolute_import, division, print_function # Author: Jacob Kristhammar, 2010 import base64 @@ -30,8 +30,8 @@ import zlib from tornado.concurrent import TracebackFuture from tornado.escape import utf8, native_str, to_unicode -from tornado import httpclient, httputil -from tornado.ioloop import IOLoop +from tornado import gen, httpclient, httputil +from tornado.ioloop import IOLoop, PeriodicCallback from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log from tornado import simple_httpclient @@ -65,6 +65,10 @@ class WebSocketHandler(tornado.web.RequestHandler): override `open` and `on_close` to handle opened and closed connections. + Custom upgrade response headers can be sent by overriding + `~tornado.web.RequestHandler.set_default_headers` or + `~tornado.web.RequestHandler.prepare`. + See http://dev.w3.org/html5/websockets/ for details on the JavaScript interface. The protocol is specified at http://tools.ietf.org/html/rfc6455. @@ -122,6 +126,17 @@ class WebSocketHandler(tornado.web.RequestHandler): to show the "accept this certificate" dialog but has nowhere to show it. You must first visit a regular HTML page using the same certificate to accept it before the websocket connection will succeed. + + If the application setting ``websocket_ping_interval`` has a non-zero + value, a ping will be sent periodically, and the connection will be + closed if a response is not received before the ``websocket_ping_timeout``. + + Messages larger than the ``websocket_max_message_size`` application setting + (default 10MiB) will not be accepted. + + .. versionchanged:: 4.5 + Added ``websocket_ping_interval``, ``websocket_ping_timeout``, and + ``websocket_max_message_size``. """ def __init__(self, application, request, **kwargs): super(WebSocketHandler, self).__init__(application, request, **kwargs) @@ -176,18 +191,42 @@ class WebSocketHandler(tornado.web.RequestHandler): gen_log.debug(log_msg) return - self.stream = self.request.connection.detach() - self.stream.set_close_callback(self.on_connection_close) - self.ws_connection = self.get_websocket_protocol() if self.ws_connection: self.ws_connection.accept_connection() else: - if not self.stream.closed(): - self.stream.write(tornado.escape.utf8( - "HTTP/1.1 426 Upgrade Required\r\n" - "Sec-WebSocket-Version: 7, 8, 13\r\n\r\n")) - self.stream.close() + self.set_status(426, "Upgrade Required") + self.set_header("Sec-WebSocket-Version", "7, 8, 13") + self.finish() + + stream = None + + @property + def ping_interval(self): + """The interval for websocket keep-alive pings. + + Set websocket_ping_interval = 0 to disable pings. + """ + return self.settings.get('websocket_ping_interval', None) + + @property + def ping_timeout(self): + """If no ping is received in this many seconds, + close the websocket connection (VPNs, etc. can fail to cleanly close ws connections). + Default is max of 3 pings or 30 seconds. + """ + return self.settings.get('websocket_ping_timeout', None) + + @property + def max_message_size(self): + """Maximum allowed message size. + + If the remote peer sends a message larger than this, the connection + will be closed. + + Default is 10MiB. + """ + return self.settings.get('websocket_max_message_size', None) def write_message(self, message, binary=False): """Sends the given message to the client of this Web Socket. @@ -231,11 +270,22 @@ class WebSocketHandler(tornado.web.RequestHandler): If this method returns None (the default), compression will be disabled. If it returns a dict (even an empty one), it will be enabled. The contents of the dict may be used to - control the memory and CPU usage of the compression, - but no such options are currently implemented. + control the following compression options: + + ``compression_level`` specifies the compression level. + + ``mem_level`` specifies the amount of memory used for the internal compression state. + + These parameters are documented in details here: + https://docs.python.org/3.6/library/zlib.html#zlib.compressobj .. versionadded:: 4.1 + + .. versionchanged:: 4.5 + + Added ``compression_level`` and ``mem_level``. """ + # TODO: Add wbits option. return None def open(self, *args, **kwargs): @@ -251,6 +301,10 @@ class WebSocketHandler(tornado.web.RequestHandler): """Handle incoming messages on the WebSocket This method must be overridden. + + .. versionchanged:: 4.5 + + ``on_message`` can be a coroutine. """ raise NotImplementedError @@ -264,6 +318,10 @@ class WebSocketHandler(tornado.web.RequestHandler): """Invoked when the response to a ping frame is received.""" pass + def on_ping(self, data): + """Invoked when the a ping frame is received.""" + pass + def on_close(self): """Invoked when the WebSocket is closed. @@ -319,7 +377,7 @@ class WebSocketHandler(tornado.web.RequestHandler): This is an important security measure; don't disable it without understanding the security implications. In - particular, if your authenticatino is cookie-based, you + particular, if your authentication is cookie-based, you must either restrict the origins allowed by ``check_origin()`` or implement your own XSRF-like protection for websocket connections. See `these @@ -376,6 +434,16 @@ class WebSocketHandler(tornado.web.RequestHandler): if not self._on_close_called: self._on_close_called = True self.on_close() + self._break_cycles() + + def _break_cycles(self): + # WebSocketHandlers call finish() early, but we don't want to + # break up reference cycles (which makes it impossible to call + # self.render_string) until after we've really closed the + # connection (if it was established in the first place, + # indicated by status code 101). + if self.get_status() != 101 or self._on_close_called: + super(WebSocketHandler, self)._break_cycles() def send_error(self, *args, **kwargs): if self.stream is None: @@ -393,18 +461,17 @@ class WebSocketHandler(tornado.web.RequestHandler): return WebSocketProtocol13( self, compression_options=self.get_compression_options()) + def _attach_stream(self): + self.stream = self.request.connection.detach() + self.stream.set_close_callback(self.on_connection_close) + # disable non-WS methods + for method in ["write", "redirect", "set_header", "set_cookie", + "set_status", "flush", "finish"]: + setattr(self, method, _raise_not_supported_for_websockets) -def _wrap_method(method): - def _disallow_for_websocket(self, *args, **kwargs): - if self.stream is None: - method(self, *args, **kwargs) - else: - raise RuntimeError("Method not supported for Web Sockets") - return _disallow_for_websocket -for method in ["write", "redirect", "set_header", "set_cookie", - "set_status", "flush", "finish"]: - setattr(WebSocketHandler, method, - _wrap_method(getattr(WebSocketHandler, method))) + +def _raise_not_supported_for_websockets(*args, **kwargs): + raise RuntimeError("Method not supported for Web Sockets") class WebSocketProtocol(object): @@ -420,14 +487,20 @@ class WebSocketProtocol(object): def _run_callback(self, callback, *args, **kwargs): """Runs the given callback with exception handling. - On error, aborts the websocket connection and returns False. + If the callback is a coroutine, returns its Future. On error, aborts the + websocket connection and returns None. """ try: - callback(*args, **kwargs) + result = callback(*args, **kwargs) except Exception: app_log.error("Uncaught exception in %s", - self.request.path, exc_info=True) + getattr(self.request, 'path', None), exc_info=True) self._abort() + else: + if result is not None: + result = gen.convert_yielded(result) + self.stream.io_loop.add_future(result, lambda f: f.result()) + return result def on_connection_close(self): self._abort() @@ -441,7 +514,7 @@ class WebSocketProtocol(object): class _PerMessageDeflateCompressor(object): - def __init__(self, persistent, max_wbits): + def __init__(self, persistent, max_wbits, compression_options=None): if max_wbits is None: max_wbits = zlib.MAX_WBITS # There is no symbolic constant for the minimum wbits value. @@ -449,14 +522,24 @@ class _PerMessageDeflateCompressor(object): raise ValueError("Invalid max_wbits value %r; allowed range 8-%d", max_wbits, zlib.MAX_WBITS) self._max_wbits = max_wbits + + if compression_options is None or 'compression_level' not in compression_options: + self._compression_level = tornado.web.GZipContentEncoding.GZIP_LEVEL + else: + self._compression_level = compression_options['compression_level'] + + if compression_options is None or 'mem_level' not in compression_options: + self._mem_level = 8 + else: + self._mem_level = compression_options['mem_level'] + if persistent: self._compressor = self._create_compressor() else: self._compressor = None def _create_compressor(self): - return zlib.compressobj(tornado.web.GZipContentEncoding.GZIP_LEVEL, - zlib.DEFLATED, -self._max_wbits) + return zlib.compressobj(self._compression_level, zlib.DEFLATED, -self._max_wbits, self._mem_level) def compress(self, data): compressor = self._compressor or self._create_compressor() @@ -467,7 +550,7 @@ class _PerMessageDeflateCompressor(object): class _PerMessageDeflateDecompressor(object): - def __init__(self, persistent, max_wbits): + def __init__(self, persistent, max_wbits, compression_options=None): if max_wbits is None: max_wbits = zlib.MAX_WBITS if not (8 <= max_wbits <= zlib.MAX_WBITS): @@ -526,6 +609,9 @@ class WebSocketProtocol13(WebSocketProtocol): # the effect of compression, frame overhead, and control frames. self._wire_bytes_in = 0 self._wire_bytes_out = 0 + self.ping_callback = None + self.last_ping = 0 + self.last_pong = 0 def accept_connection(self): try: @@ -562,46 +648,42 @@ class WebSocketProtocol13(WebSocketProtocol): self.request.headers.get("Sec-Websocket-Key")) def _accept_connection(self): - subprotocol_header = '' subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '') subprotocols = [s.strip() for s in subprotocols.split(',')] if subprotocols: selected = self.handler.select_subprotocol(subprotocols) if selected: assert selected in subprotocols - subprotocol_header = ("Sec-WebSocket-Protocol: %s\r\n" - % selected) + self.handler.set_header("Sec-WebSocket-Protocol", selected) - extension_header = '' extensions = self._parse_extensions_header(self.request.headers) for ext in extensions: if (ext[0] == 'permessage-deflate' and self._compression_options is not None): # TODO: negotiate parameters if compression_options # specifies limits. - self._create_compressors('server', ext[1]) + self._create_compressors('server', ext[1], self._compression_options) if ('client_max_window_bits' in ext[1] and ext[1]['client_max_window_bits'] is None): # Don't echo an offered client_max_window_bits # parameter with no value. del ext[1]['client_max_window_bits'] - extension_header = ('Sec-WebSocket-Extensions: %s\r\n' % - httputil._encode_header( - 'permessage-deflate', ext[1])) + self.handler.set_header("Sec-WebSocket-Extensions", + httputil._encode_header( + 'permessage-deflate', ext[1])) break - if self.stream.closed(): - self._abort() - return - self.stream.write(tornado.escape.utf8( - "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Accept: %s\r\n" - "%s%s" - "\r\n" % (self._challenge_response(), - subprotocol_header, extension_header))) + self.handler.clear_header("Content-Type") + self.handler.set_status(101) + self.handler.set_header("Upgrade", "websocket") + self.handler.set_header("Connection", "Upgrade") + self.handler.set_header("Sec-WebSocket-Accept", self._challenge_response()) + self.handler.finish() + self.handler._attach_stream() + self.stream = self.handler.stream + + self.start_pinging() self._run_callback(self.handler.open, *self.handler.open_args, **self.handler.open_kwargs) self._receive_frame() @@ -631,7 +713,7 @@ class WebSocketProtocol13(WebSocketProtocol): else: raise ValueError("unsupported extension %r", ext) - def _get_compressor_options(self, side, agreed_parameters): + def _get_compressor_options(self, side, agreed_parameters, compression_options=None): """Converts a websocket agreed_parameters set to keyword arguments for our compressor objects. """ @@ -642,9 +724,10 @@ class WebSocketProtocol13(WebSocketProtocol): options['max_wbits'] = zlib.MAX_WBITS else: options['max_wbits'] = int(wbits_header) + options['compression_options'] = compression_options return options - def _create_compressors(self, side, agreed_parameters): + def _create_compressors(self, side, agreed_parameters, compression_options=None): # TODO: handle invalid parameters gracefully allowed_keys = set(['server_no_context_takeover', 'client_no_context_takeover', @@ -655,9 +738,9 @@ class WebSocketProtocol13(WebSocketProtocol): raise ValueError("unsupported compression parameter %r" % key) other_side = 'client' if (side == 'server') else 'server' self._compressor = _PerMessageDeflateCompressor( - **self._get_compressor_options(side, agreed_parameters)) + **self._get_compressor_options(side, agreed_parameters, compression_options)) self._decompressor = _PerMessageDeflateDecompressor( - **self._get_compressor_options(other_side, agreed_parameters)) + **self._get_compressor_options(other_side, agreed_parameters, compression_options)) def _write_frame(self, fin, opcode, data, flags=0): if fin: @@ -738,8 +821,7 @@ class WebSocketProtocol13(WebSocketProtocol): if self._masked_frame: self.stream.read_bytes(4, self._on_masking_key) else: - self.stream.read_bytes(self._frame_length, - self._on_frame_data) + self._read_frame_data(False) elif payloadlen == 126: self.stream.read_bytes(2, self._on_frame_length_16) elif payloadlen == 127: @@ -747,6 +829,17 @@ class WebSocketProtocol13(WebSocketProtocol): except StreamClosedError: self._abort() + def _read_frame_data(self, masked): + new_len = self._frame_length + if self._fragmented_message_buffer is not None: + new_len += len(self._fragmented_message_buffer) + if new_len > (self.handler.max_message_size or 10 * 1024 * 1024): + self.close(1009, "message too big") + return + self.stream.read_bytes( + self._frame_length, + self._on_masked_frame_data if masked else self._on_frame_data) + def _on_frame_length_16(self, data): self._wire_bytes_in += len(data) self._frame_length = struct.unpack("!H", data)[0] @@ -754,7 +847,7 @@ class WebSocketProtocol13(WebSocketProtocol): if self._masked_frame: self.stream.read_bytes(4, self._on_masking_key) else: - self.stream.read_bytes(self._frame_length, self._on_frame_data) + self._read_frame_data(False) except StreamClosedError: self._abort() @@ -765,7 +858,7 @@ class WebSocketProtocol13(WebSocketProtocol): if self._masked_frame: self.stream.read_bytes(4, self._on_masking_key) else: - self.stream.read_bytes(self._frame_length, self._on_frame_data) + self._read_frame_data(False) except StreamClosedError: self._abort() @@ -773,8 +866,7 @@ class WebSocketProtocol13(WebSocketProtocol): self._wire_bytes_in += len(data) self._frame_mask = data try: - self.stream.read_bytes(self._frame_length, - self._on_masked_frame_data) + self._read_frame_data(True) except StreamClosedError: self._abort() @@ -783,6 +875,8 @@ class WebSocketProtocol13(WebSocketProtocol): self._on_frame_data(_websocket_mask(self._frame_mask, data)) def _on_frame_data(self, data): + handled_future = None + self._wire_bytes_in += len(data) if self._frame_opcode_is_control: # control frames may be interleaved with a series of fragmented @@ -815,12 +909,18 @@ class WebSocketProtocol13(WebSocketProtocol): self._fragmented_message_buffer = data if self._final_frame: - self._handle_message(opcode, data) + handled_future = self._handle_message(opcode, data) if not self.client_terminated: - self._receive_frame() + if handled_future: + # on_message is a coroutine, process more frames once it's done. + handled_future.add_done_callback( + lambda future: self._receive_frame()) + else: + self._receive_frame() def _handle_message(self, opcode, data): + """Execute on_message, returning its Future if it is a coroutine.""" if self.client_terminated: return @@ -835,11 +935,11 @@ class WebSocketProtocol13(WebSocketProtocol): except UnicodeDecodeError: self._abort() return - self._run_callback(self.handler.on_message, decoded) + return self._run_callback(self.handler.on_message, decoded) elif opcode == 0x2: # Binary data self._message_bytes_in += len(data) - self._run_callback(self.handler.on_message, data) + return self._run_callback(self.handler.on_message, data) elif opcode == 0x8: # Close self.client_terminated = True @@ -852,9 +952,11 @@ class WebSocketProtocol13(WebSocketProtocol): elif opcode == 0x9: # Ping self._write_frame(True, 0xA, data) + self._run_callback(self.handler.on_ping, data) elif opcode == 0xA: # Pong - self._run_callback(self.handler.on_pong, data) + self.last_pong = IOLoop.current().time() + return self._run_callback(self.handler.on_pong, data) else: self._abort() @@ -883,6 +985,51 @@ class WebSocketProtocol13(WebSocketProtocol): self._waiting = self.stream.io_loop.add_timeout( self.stream.io_loop.time() + 5, self._abort) + @property + def ping_interval(self): + interval = self.handler.ping_interval + if interval is not None: + return interval + return 0 + + @property + def ping_timeout(self): + timeout = self.handler.ping_timeout + if timeout is not None: + return timeout + return max(3 * self.ping_interval, 30) + + def start_pinging(self): + """Start sending periodic pings to keep the connection alive""" + if self.ping_interval > 0: + self.last_ping = self.last_pong = IOLoop.current().time() + self.ping_callback = PeriodicCallback( + self.periodic_ping, self.ping_interval * 1000) + self.ping_callback.start() + + def periodic_ping(self): + """Send a ping to keep the websocket alive + + Called periodically if the websocket_ping_interval is set and non-zero. + """ + if self.stream.closed() and self.ping_callback is not None: + self.ping_callback.stop() + return + + # Check for timeout on pong. Make sure that we really have + # sent a recent ping in case the machine with both server and + # client has been suspended since the last ping. + now = IOLoop.current().time() + since_last_pong = now - self.last_pong + since_last_ping = now - self.last_ping + if (since_last_ping < 2 * self.ping_interval and + since_last_pong > self.ping_timeout): + self.close() + return + + self.write_ping(b'') + self.last_ping = now + class WebSocketClientConnection(simple_httpclient._HTTPConnection): """WebSocket client connection. @@ -891,7 +1038,8 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): `websocket_connect` function instead. """ def __init__(self, io_loop, request, on_message_callback=None, - compression_options=None): + compression_options=None, ping_interval=None, ping_timeout=None, + max_message_size=None): self.compression_options = compression_options self.connect_future = TracebackFuture() self.protocol = None @@ -900,6 +1048,9 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): self.key = base64.b64encode(os.urandom(16)) self._on_message_callback = on_message_callback self.close_code = self.close_reason = None + self.ping_interval = ping_interval + self.ping_timeout = ping_timeout + self.max_message_size = max_message_size scheme, sep, rest = request.url.partition(':') scheme = {'ws': 'http', 'wss': 'https'}[scheme] @@ -963,6 +1114,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): self.headers = headers self.protocol = self.get_websocket_protocol() self.protocol._process_server_headers(self.key, self.headers) + self.protocol.start_pinging() self.protocol._receive_frame() if self._timeout is not None: @@ -1016,13 +1168,18 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): def on_pong(self, data): pass + def on_ping(self, data): + pass + def get_websocket_protocol(self): return WebSocketProtocol13(self, mask_outgoing=True, compression_options=self.compression_options) def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None, - on_message_callback=None, compression_options=None): + on_message_callback=None, compression_options=None, + ping_interval=None, ping_timeout=None, + max_message_size=None): """Client-side websocket support. Takes a url and returns a Future whose result is a @@ -1051,6 +1208,10 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None, .. versionchanged:: 4.1 Added ``compression_options`` and ``on_message_callback``. The ``io_loop`` argument is deprecated. + + .. versionchanged:: 4.5 + Added the ``ping_interval``, ``ping_timeout``, and ``max_message_size`` + arguments, which have the same meaning as in `WebSocketHandler`. """ if io_loop is None: io_loop = IOLoop.current() @@ -1066,7 +1227,10 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None, request, httpclient.HTTPRequest._DEFAULTS) conn = WebSocketClientConnection(io_loop, request, on_message_callback=on_message_callback, - compression_options=compression_options) + compression_options=compression_options, + ping_interval=ping_interval, + ping_timeout=ping_timeout, + max_message_size=max_message_size) if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future diff --git a/lib/tornado/wsgi.py b/lib/tornado/wsgi.py index e9ead300..68a7615a 100644 --- a/lib/tornado/wsgi.py +++ b/lib/tornado/wsgi.py @@ -29,7 +29,7 @@ provides WSGI support in two ways: and Tornado handlers in a single server. """ -from __future__ import absolute_import, division, print_function, with_statement +from __future__ import absolute_import, division, print_function import sys from io import BytesIO diff --git a/lib/tvdb_api/tvdb_api.py b/lib/tvdb_api/tvdb_api.py index b4c65286..91b7e741 100644 --- a/lib/tvdb_api/tvdb_api.py +++ b/lib/tvdb_api/tvdb_api.py @@ -5,40 +5,40 @@ # repository:http://github.com/dbr/tvdb_api # license:unlicense (http://unlicense.org/) -import traceback from functools import wraps __author__ = 'dbr/Ben' -__version__ = '1.9' +__version__ = '2.0' +__api_version__ = '2.1.2' import os import time import getpass -import StringIO import tempfile import warnings import logging -import zipfile import requests import requests.exceptions +import datetime +import re -try: - import gzip -except ImportError: - gzip = None +from sickbeard.helpers import getURL, tryInt +import sickbeard from lib.dateutil.parser import parse from lib.cachecontrol import CacheControl, caches -from lib.etreetodict import ConvertXmlToDict from tvdb_ui import BaseUI, ConsoleUI -from tvdb_exceptions import (tvdb_error, tvdb_shownotfound, - tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound) - -from sickbeard import logger +from tvdb_exceptions import ( + tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, + tvdb_attributenotfound, tvdb_tokenexpired) -def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logr=None): +def log(): + return logging.getLogger('tvdb_api') + + +def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): """Retry calling the decorated function using an exponential backoff. http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ @@ -54,8 +54,8 @@ def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logr=None): :param backoff: backoff multiplier e.g. value of 2 will double the delay each retry :type backoff: int - :param logr: logger to use. If None, print - :type logr: logging.Logger instance + :param logger: logger to use. If None, print + :type logger: logging.Logger instance """ def deco_retry(f): @@ -63,19 +63,28 @@ def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logr=None): @wraps(f) def f_retry(*args, **kwargs): mtries, mdelay = tries, delay + auth_error = 0 while mtries > 1: try: return f(*args, **kwargs) except ExceptionToCheck, e: - msg = 'TVDB_API :: %s, Retrying in %d seconds...' % (str(e), mdelay) - if logr: - logger.log(msg, logger.WARNING) + msg = '%s, Retrying in %d seconds...' % (str(e), mdelay) + if logger: + logger.warning(msg) else: print msg time.sleep(mdelay) - mtries -= 1 - mdelay *= backoff - return f(*args, **kwargs) + if isinstance(e, tvdb_tokenexpired) and not auth_error: + auth_error += 1 + else: + mtries -= 1 + mdelay *= backoff + try: + return f(*args, **kwargs) + except tvdb_tokenexpired: + if not auth_error: + return f(*args, **kwargs) + raise tvdb_tokenexpired return f_retry # true decorator @@ -86,7 +95,8 @@ class ShowContainer(dict): """Simple dict that holds a series of Show instances """ - def __init__(self): + def __init__(self, **kwargs): + super(ShowContainer, self).__init__(**kwargs) self._stack = [] self._lastgc = time.time() @@ -137,7 +147,7 @@ class Show(dict): return dict.__getitem__(self.data, key) # Data wasn't found, raise appropriate error - if isinstance(key, int) or key.isdigit(): + if isinstance(key, (int, long)) or isinstance(key, basestring) and key.isdigit(): # Episode number x was not found raise tvdb_seasonnotfound('Could not find season %s' % (repr(key))) else: @@ -145,7 +155,7 @@ class Show(dict): # doesn't exist, so attribute error. raise tvdb_attributenotfound('Cannot find attribute %s' % (repr(key))) - def airedOn(self, date): + def aired_on(self, date): ret = self.search(str(date), 'firstaired') if 0 == len(ret): raise tvdb_episodenotfound('Could not find any episodes that aired on %s' % date) @@ -212,9 +222,10 @@ class Show(dict): class Season(dict): - def __init__(self, show=None): + def __init__(self, show=None, **kwargs): """The show attribute points to the parent show """ + super(Season, self).__init__(**kwargs) self.show = show def __repr__(self): @@ -251,9 +262,10 @@ class Season(dict): class Episode(dict): - def __init__(self, season=None): + def __init__(self, season=None, **kwargs): """The season attribute points to the parent season """ + super(Episode, self).__init__(**kwargs) self.season = season def __repr__(self): @@ -301,7 +313,7 @@ class Episode(dict): raise TypeError('must supply string to search for (contents)') term = unicode(term).lower() - for cur_key, cur_value in self.items(): + for cur_key, cur_value in self.iteritems(): cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower() if None is not key and cur_key != key: # Do not search this key @@ -337,21 +349,26 @@ class Tvdb: u'My Last Day' """ + # noinspection PyUnusedLocal def __init__(self, interactive=False, select_first=False, debug=False, cache=True, banners=False, + fanart=False, + posters=False, + seasons=False, + seasonwides=False, actors=False, custom_ui=None, language=None, search_all_languages=False, apikey=None, - forceConnect=False, - useZip=False, dvdorder=False, - proxy=None): + proxy=None, + *args, + **kwargs): """interactive (True/False): When True, uses built-in console UI is used to select the correct show. @@ -413,20 +430,12 @@ class Tvdb: tvdb_api in a larger application) See http://thetvdb.com/?tab=apiregister to get your own key - forceConnect (bool): - If true it will always try to connect to theTVDB.com even if we - recently timed out. By default it will wait one minute before - trying again, and any requests within that one minute window will - return an exception immediately. - - useZip (bool): - Download the zip archive where possibale, instead of the xml. - This is only used when all episodes are pulled. - And only the main language xml is used, the actor and banner xml are lost. """ self.shows = ShowContainer() # Holds all Show classes self.corrections = {} # Holds show-name to show_id mapping + self.show_not_found = False + self.not_found = False self.config = {} @@ -445,8 +454,6 @@ class Tvdb: self.config['search_all_languages'] = search_all_languages - self.config['useZip'] = useZip - self.config['dvdorder'] = dvdorder self.config['proxy'] = proxy @@ -463,6 +470,10 @@ class Tvdb: raise ValueError('Invalid value for Cache %r (type was %s)' % (cache, type(cache))) self.config['banners_enabled'] = banners + self.config['posters_enabled'] = posters + self.config['seasons_enabled'] = seasons + self.config['seasonwides_enabled'] = seasonwides + self.config['fanart_enabled'] = fanart self.config['actors_enabled'] = actors if self.config['debug_enabled']: @@ -498,26 +509,39 @@ class Tvdb: # The following url_ configs are based of the # http://thetvdb.com/wiki/index.php/Programmers_API - self.config['base_url'] = 'http://thetvdb.com' + self.config['base_url'] = 'https://api.thetvdb.com/' - if self.config['search_all_languages']: - self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config - self.config['params_get_series'] = {'seriesname': '', 'language': 'all'} - else: - self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config - self.config['params_get_series'] = {'seriesname': '', 'language': self.config['language']} + self.config['url_get_series'] = '%(base_url)s/search/series' % self.config + self.config['params_get_series'] = {'name': ''} - self.config['url_epInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml' % self.config - self.config['url_epInfo_zip'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip' % self.config + self.config['url_epInfo'] = '%(base_url)sseries/%%s/episodes?page=%%s' % self.config - self.config['url_seriesInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml' % self.config - self.config['url_actorsInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/actors.xml' % self.config + self.config['url_seriesInfo'] = '%(base_url)sseries/%%s' % self.config + self.config['url_actorsInfo'] = '%(base_url)sseries/%%s/actors' % self.config - self.config['url_seriesBanner'] = u'%(base_url)s/api/%(apikey)s/series/%%s/banners.xml' % self.config - self.config['url_artworkPrefix'] = u'%(base_url)s/banners/%%s' % self.config + self.config['url_seriesBanner'] = '%(base_url)sseries/%%s/images/query?keyType=%%s' % self.config + self.config['url_artworkPrefix'] = 'https://thetvdb.com/banners/%s' - def log(self, msg, log_level=logger.DEBUG): - logger.log('TVDB_API :: %s' % (msg.replace(self.config['apikey'], '')), log_level=log_level) + def get_new_token(self): + token = sickbeard.THETVDB_V2_API_TOKEN.get('token', None) + dt = sickbeard.THETVDB_V2_API_TOKEN.get('datetime', datetime.datetime.fromordinal(1)) + url = '%s%s' % (self.config['base_url'], 'login') + params = {'apikey': self.config['apikey']} + resp = getURL(url.strip(), post_json=params, json=True) + if resp: + if 'token' in resp: + token = resp['token'] + dt = datetime.datetime.now() + + return {'token': token, 'datetime': dt} + + def get_token(self): + if sickbeard.THETVDB_V2_API_TOKEN.get('token') is None or datetime.datetime.now() - sickbeard.THETVDB_V2_API_TOKEN.get( + 'datetime', datetime.datetime.fromordinal(1)) > datetime.timedelta(hours=23): + sickbeard.THETVDB_V2_API_TOKEN = self.get_new_token() + if not sickbeard.THETVDB_V2_API_TOKEN.get('token'): + raise tvdb_error('Could not get Authentification Token') + return sickbeard.THETVDB_V2_API_TOKEN.get('token') @staticmethod def _get_temp_dir(): @@ -535,9 +559,9 @@ class Tvdb: return os.path.join(tempfile.gettempdir(), 'tvdb_api-%s' % uid) - @retry(tvdb_error) + @retry((tvdb_error, tvdb_tokenexpired)) def _load_url(self, url, params=None, language=None): - self.log('Retrieving URL %s' % url) + log().debug('Retrieving URL %s' % url) session = requests.session() @@ -545,56 +569,79 @@ class Tvdb: session = CacheControl(session, cache=caches.FileCache(self.config['cache_location'])) if self.config['proxy']: - self.log('Using proxy for URL: %s' % url) + log().debug('Using proxy for URL: %s' % url) session.proxies = {'http': self.config['proxy'], 'https': self.config['proxy']} - session.headers.update({'Accept-Encoding': 'gzip,deflate'}) + session.headers.update({'Accept-Encoding': 'gzip,deflate', 'Authorization': 'Bearer %s' % self.get_token(), + 'Accept': 'application/vnd.thetvdb.v%s' % __api_version__}) + if None is not language and language in self.config['valid_languages']: + session.headers.update({'Accept-Language': language}) + + resp = None + if re.search(re.escape(self.config['url_seriesInfo']).replace('%s', '.*'), url): + self.show_not_found = False + self.not_found = False try: - resp = session.get(url.strip(), params=params) - except requests.exceptions.HTTPError, e: - raise tvdb_error('HTTP error %s while loading URL %s' % (e.errno, url)) - except requests.exceptions.ConnectionError, e: - raise tvdb_error('Connection error %s while loading URL %s' % (e.message, url)) - except requests.exceptions.Timeout, e: - raise tvdb_error('Connection timed out %s while loading URL %s' % (e.message, url)) - except Exception: - raise tvdb_error('Unknown exception while loading URL %s: %s' % (url, traceback.format_exc())) + resp = getURL(url.strip(), params=params, session=session, json=True, raise_status_code=True, + raise_exceptions=True) + except requests.exceptions.HTTPError as e: + if 401 == e.response.status_code: + # token expired, get new token, raise error to retry + sickbeard.THETVDB_V2_API_TOKEN = self.get_new_token() + raise tvdb_tokenexpired + elif 404 == e.response.status_code: + if re.search(re.escape(self.config['url_seriesInfo']).replace('%s', '.*'), url): + self.show_not_found = True + self.not_found = True + elif 404 != e.response.status_code: + raise tvdb_error + except (StandardError, Exception): + raise tvdb_error - def process_data(data): - te = ConvertXmlToDict(data) - if isinstance(te, dict) and 'Data' in te and isinstance(te['Data'], dict) \ - and 'Series' in te['Data'] and isinstance(te['Data']['Series'], dict) \ - and 'FirstAired' in te['Data']['Series']: - try: - value = parse(te['Data']['Series']['FirstAired'], fuzzy=True).strftime('%Y-%m-%d') - except (StandardError, Exception): - value = None - te['Data']['Series']['firstaired'] = value - return te + map_show = {'airstime': 'airs_time', 'airsdayofweek': 'airs_dayofweek', 'imdbid': 'imdb_id'} - if resp.ok: - if 'application/zip' in resp.headers.get('Content-Type', ''): - try: - # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20] - self.log('We received a zip file unpacking now ...') - zipdata = StringIO.StringIO() - zipdata.write(resp.content) - myzipfile = zipfile.ZipFile(zipdata) - return process_data(myzipfile.read('%s.xml' % language)) - except zipfile.BadZipfile: - raise tvdb_error('Bad zip file received from thetvdb.com, could not read it') - else: - try: - return process_data(resp.content.strip()) - except (StandardError, Exception): - return dict([(u'data', None)]) + def map_show_keys(data): + for k, v in data.iteritems(): + k_org = k + k = k.lower() + if None is not v: + if k in ['banner', 'fanart', 'poster'] and v: + v = self.config['url_artworkPrefix'] % v + elif 'genre' == k: + v = '|%s|' % '|'.join([self._clean_data(c) for c in v if isinstance(c, basestring)]) + elif 'firstaired' == k: + if v: + try: + v = parse(v, fuzzy=True).strftime('%Y-%m-%d') + except (StandardError, Exception): + v = None + else: + v = None + else: + v = self._clean_data(v) + if k in map_show: + k = map_show[k] + if k_org is not k: + del(data[k_org]) + data[k] = v + return data + + if resp: + if isinstance(resp['data'], dict): + resp['data'] = map_show_keys(resp['data']) + elif isinstance(resp['data'], list): + for idx, row in enumerate(resp['data']): + if isinstance(row, dict): + resp['data'][idx] = map_show_keys(row) + return resp + return dict([(u'data', None)]) def _getetsrc(self, url, params=None, language=None): - """Loads a URL using caching, returns an ElementTree of the source + """Loads a URL using caching """ try: - src = self._load_url(url, params=params, language=language).values()[0] + src = self._load_url(url, params=params, language=language) return src except (StandardError, Exception): return [] @@ -622,40 +669,41 @@ class Tvdb: self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) self.shows[sid][seas][ep][attrib] = value - def _set_show_data(self, sid, key, value): + def _set_show_data(self, sid, key, value, add=False): """Sets self.shows[sid] to a new Show instance, or sets the data """ if sid not in self.shows: self.shows[sid] = Show() - self.shows[sid].data[key] = value + if add and isinstance(self.shows[sid].data, dict) and key in self.shows[sid].data: + self.shows[sid].data[key].update(value) + else: + self.shows[sid].data[key] = value - @staticmethod - def _clean_data(data): - """Cleans up strings returned by TheTVDB.com + def _clean_data(self, data): + """Cleans up strings, lists, dicts returned Issues corrected: - Replaces & with & - Trailing whitespace """ - return data if not isinstance(data, basestring) else data.strip().replace(u'&', u'&') - - def _get_url_artwork(self, image): - return image and (self.config['url_artworkPrefix'] % image) or image + if isinstance(data, list): + return [self._clean_data(d) for d in data] + if isinstance(data, dict): + return {k: self._clean_data(v) for k, v in data.iteritems()} + return data if not isinstance(data, (str, unicode)) else data.strip().replace(u'&', u'&') def search(self, series): """This searches TheTVDB.com for the series name and returns the result list """ series = series.encode('utf-8') - self.log('Searching for show %s' % series) - self.config['params_get_series']['seriesname'] = series + self.config['params_get_series']['name'] = series + log().debug('Searching for show %s' % series) try: - series_found = self._getetsrc(self.config['url_get_series'], self.config['params_get_series']) + series_found = self._getetsrc(self.config['url_get_series'], params=self.config['params_get_series'], + language=self.config['language']) if series_found: - if not isinstance(series_found['Series'], list): - series_found['Series'] = [series_found['Series']] - series_found['Series'] = [{k.lower(): v for k, v in s.iteritems()} for s in series_found['Series']] return series_found.values()[0] except (StandardError, Exception): pass @@ -673,50 +721,31 @@ class Tvdb: all_series = [all_series] if 0 == len(all_series): - self.log('Series result returned zero') + log().debug('Series result returned zero') raise tvdb_shownotfound('Show-name search returned zero results (cannot find show on TVDB)') if None is not self.config['custom_ui']: - self.log('Using custom UI %s' % (repr(self.config['custom_ui']))) + log().debug('Using custom UI %s' % (repr(self.config['custom_ui']))) custom_ui = self.config['custom_ui'] ui = custom_ui(config=self.config) else: if not self.config['interactive']: - self.log('Auto-selecting first search result using BaseUI') + log().debug('Auto-selecting first search result using BaseUI') ui = BaseUI(config=self.config) else: - self.log('Interactively selecting show using ConsoleUI') + log().debug('Interactively selecting show using ConsoleUI') ui = ConsoleUI(config=self.config) - return ui.selectSeries(all_series) + return ui.select_series(all_series) - def _parse_banners(self, sid): - """Parses banners XML, from - http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml - - Banners are retrieved using t['show name]['_banners'], for example: - - >> t = Tvdb(banners = True) - >> t['scrubs']['_banners'].keys() - ['fanart', 'poster', 'series', 'season'] - >> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] - u'http://thetvdb.com/banners/posters/76156-2.jpg' - >> - - Any key starting with an underscore has been processed (not the raw - data from the XML) - - This interface will be improved in future versions. - """ - self.log('Getting season banners for %s' % sid) - banners_et = self._getetsrc(self.config['url_seriesBanner'] % sid) + def _parse_banners(self, sid, img_list): banners = {} try: - for cur_banner in banners_et['banner']: + for cur_banner in img_list: bid = cur_banner['id'] - btype = cur_banner['bannertype'] - btype2 = cur_banner['bannertype2'] + btype = (cur_banner['keytype'], 'banner')['series' == cur_banner['keytype']] + btype2 = (cur_banner['resolution'], tryInt(cur_banner['subkey'], cur_banner['subkey']))[btype in ('season', 'seasonwide')] if None is btype or None is btype2: continue if btype not in banners: @@ -726,60 +755,37 @@ class Tvdb: if bid not in banners[btype][btype2]: banners[btype][btype2][bid] = {} - for k, v in cur_banner.items(): + for k, v in cur_banner.iteritems(): if None is k or None is v: continue - k, v = k.lower(), v.lower() + k, v = k.lower(), v.lower() if isinstance(v, (str, unicode)) else v + if k == 'filename': + k = 'bannerpath' + banners[btype][btype2][bid]['_bannerpath'] = self.config['url_artworkPrefix'] % v + elif k == 'thumbnail': + k = 'thumbnailpath' + banners[btype][btype2][bid]['_thumbnailpath'] = self.config['url_artworkPrefix'] % v + elif k == 'keytype': + k = 'bannertype' banners[btype][btype2][bid][k] = v - for k, v in banners[btype][btype2][bid].items(): - if k.endswith('path'): - new_key = '_%s' % k - self.log('Transforming %s to %s' % (k, new_key)) - new_url = self._get_url_artwork(v) - banners[btype][btype2][bid][new_key] = new_url except (StandardError, Exception): pass - self._set_show_data(sid, '_banners', banners) + self._set_show_data(sid, '_banners', banners, add=True) - def _parse_actors(self, sid): - """Parsers actors XML, from - http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml - - Actors are retrieved using t['show name]['_actors'], for example: - - >> t = Tvdb(actors = True) - >> actors = t['scrubs']['_actors'] - >> type(actors) - - >> type(actors[0]) - - >> actors[0] - - >> sorted(actors[0].keys()) - ['id', 'image', 'name', 'role', 'sortorder'] - >> actors[0]['name'] - u'Zach Braff' - >> actors[0]['image'] - u'http://thetvdb.com/banners/actors/43640.jpg' - - Any key starting with an underscore has been processed (not the raw - data from the XML) - """ - self.log('Getting actors for %s' % sid) - actors_et = self._getetsrc(self.config['url_actorsInfo'] % sid) + def _parse_actors(self, sid, actor_list): cur_actors = Actors() try: - for curActorItem in actors_et['actor']: + for curActorItem in actor_list: cur_actor = Actor() - for k, v in curActorItem.items(): + for k, v in curActorItem.iteritems(): k = k.lower() if None is not v: if 'image' == k: - v = self._get_url_artwork(v) + v = self.config['url_artworkPrefix'] % v else: v = self._clean_data(v) cur_actor[k] = v @@ -795,96 +801,128 @@ class Tvdb: shows[series_id][season_number][episode_number] """ - if None is self.config['language']: - self.log('Config language is none, using show language') - if None is language: - raise tvdb_error('config[\'language\'] was None, this should not happen') - get_show_in_language = language - else: - self.log('Configured language %s override show language of %s' % (self.config['language'], language)) - get_show_in_language = self.config['language'] - # Parse show information - self.log('Getting all series data for %s' % sid) - url = (self.config['url_seriesInfo'] % (sid, language), self.config['url_epInfo%s' % ('', '_zip')[self.config['useZip']]] % (sid, language))[get_ep_info] - show_data = self._getetsrc(url, language=get_show_in_language) + log().debug('Getting all series data for %s' % sid) + url = self.config['url_seriesInfo'] % sid + show_data = self._getetsrc(url, language=language) # check and make sure we have data to process and that it contains a series name - if not len(show_data) or (isinstance(show_data, dict) and 'SeriesName' not in show_data['Series']): + if not isinstance(show_data, dict) or 'data' not in show_data or not isinstance(show_data['data'], dict) or 'seriesname' not in show_data['data']: return False - for k, v in show_data['Series'].iteritems(): - if None is not v: - if k in ['banner', 'fanart', 'poster']: - v = self._get_url_artwork(v) - else: - v = self._clean_data(v) + for k, v in show_data['data'].iteritems(): + self._set_show_data(sid, k, v) - self._set_show_data(sid, k.lower(), v) + p = '' + if self.config['posters_enabled']: + poster_data = self._getetsrc(self.config['url_seriesBanner'] % (sid, 'poster'), language=language) + if poster_data and 'data' in poster_data and poster_data['data'] and len(poster_data['data']) > 0: + poster_data['data'] = sorted(poster_data['data'], reverse=True, + key=lambda x: (x['ratingsinfo']['average'], x['ratingsinfo']['count'])) + p = self.config['url_artworkPrefix'] % poster_data['data'][0]['filename'] + self._parse_banners(sid, poster_data['data']) + if p: + self._set_show_data(sid, u'poster', p) + + b = '' + if self.config['banners_enabled']: + poster_data = self._getetsrc(self.config['url_seriesBanner'] % (sid, 'series'), language=language) + if poster_data and 'data' in poster_data and poster_data['data'] and len(poster_data['data']) > 0: + poster_data['data'] = sorted(poster_data['data'], reverse=True, + key=lambda x: (x['ratingsinfo']['average'], x['ratingsinfo']['count'])) + b = self.config['url_artworkPrefix'] % poster_data['data'][0]['filename'] + self._parse_banners(sid, poster_data['data']) + if b: + self._set_show_data(sid, u'banner', b) + + if self.config['seasons_enabled']: + poster_data = self._getetsrc(self.config['url_seriesBanner'] % (sid, 'season'), language=language) + if poster_data and 'data' in poster_data and poster_data['data'] and len(poster_data['data']) > 0: + poster_data['data'] = sorted(poster_data['data'], reverse=True, + key=lambda x: (-1 * tryInt(x['subkey']), x['ratingsinfo']['average'], x['ratingsinfo']['count'])) + self._parse_banners(sid, poster_data['data']) + + if self.config['seasonwides_enabled']: + poster_data = self._getetsrc(self.config['url_seriesBanner'] % (sid, 'seasonwide'), language=language) + if poster_data and 'data' in poster_data and poster_data['data'] and len(poster_data['data']) > 0: + poster_data['data'] = sorted(poster_data['data'], reverse=True, + key=lambda x: (-1 * tryInt(x['subkey']), x['ratingsinfo']['average'], x['ratingsinfo']['count'])) + self._parse_banners(sid, poster_data['data']) + + f = '' + if self.config['fanart_enabled']: + fanart_data = self._getetsrc(self.config['url_seriesBanner'] % (sid, 'fanart'), language=language) + if fanart_data and 'data' in fanart_data and fanart_data['data'] and len(fanart_data['data']) > 0: + fanart_data['data'] = sorted(fanart_data['data'], reverse=True, + key=lambda x: (x['ratingsinfo']['average'], x['ratingsinfo']['count'])) + f = self.config['url_artworkPrefix'] % fanart_data['data'][0]['filename'] + self._parse_banners(sid, fanart_data['data']) + if f: + self._set_show_data(sid, u'fanart', f) + + if self.config['actors_enabled']: + actor_data = self._getetsrc(self.config['url_actorsInfo'] % sid, language=language) + if actor_data and 'data' in actor_data and actor_data['data'] and len(actor_data['data']) > 0: + a = '|%s|' % '|'.join([n.get('name', '') for n in sorted( + actor_data['data'], key=lambda x: x['sortorder'])]) + self._parse_actors(sid, actor_data['data']) + else: + a = '||' + self._set_show_data(sid, u'actors', a) if get_ep_info: - # Parse banners - if self.config['banners_enabled']: - self._parse_banners(sid) - - # Parse actors - if self.config['actors_enabled']: - self._parse_actors(sid) - # Parse episode data - self.log('Getting all episodes of %s' % sid) + log().debug('Getting all episodes of %s' % sid) - if 'Episode' not in show_data: - return False + page = 1 + episodes = [] + while page is not None: + episode_data = self._getetsrc(self.config['url_epInfo'] % (sid, page), language=language) + if [] is episode_data: + raise tvdb_error('Exception retrieving episodes for show') + if isinstance(episode_data, dict) and episode_data['data'] is not None: + episodes.extend(episode_data['data']) + page = episode_data['links']['next'] if isinstance(episode_data, dict) \ + and 'links' in episode_data and 'next' in episode_data['links'] else None - episodes = show_data['Episode'] - if not isinstance(episodes, list): - episodes = [episodes] + ep_map_keys = {'absolutenumber': u'absolute_number', 'airedepisodenumber': u'episodenumber', + 'airedseason': u'seasonnumber', 'airedseasonid': u'seasonid', + 'dvdepisodenumber': u'dvd_episodenumber', 'dvdseason': u'dvd_season'} - dvd_order = {'dvd': [], 'network': []} for cur_ep in episodes: if self.config['dvdorder']: - use_dvd = cur_ep['DVD_season'] not in (None, '') and cur_ep['DVD_episodenumber'] not in (None, '') + log().debug('Using DVD ordering.') + use_dvd = None is not cur_ep.get('dvdseason') and None is not cur_ep.get('dvdepisodenumber') else: use_dvd = False if use_dvd: - elem_seasnum, elem_epno = cur_ep['DVD_season'], cur_ep['DVD_episodenumber'] + elem_seasnum, elem_epno = cur_ep.get('dvdseason'), cur_ep.get('dvdepisodenumber') else: - elem_seasnum, elem_epno = cur_ep['SeasonNumber'], cur_ep['EpisodeNumber'] + elem_seasnum, elem_epno = cur_ep.get('airedseason'), cur_ep.get('airedepisodenumber') if None is elem_seasnum or None is elem_epno: - self.log('An episode has incomplete season/episode number (season: %r, episode: %r)' % ( - elem_seasnum, elem_epno), logger.WARNING) + log().warning('An episode has incomplete season/episode number (season: %r, episode: %r)' % ( + elem_seasnum, elem_epno)) continue # Skip to next episode # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data seas_no = int(float(elem_seasnum)) ep_no = int(float(elem_epno)) - if self.config['dvdorder']: - dvd_order[('network', 'dvd')[use_dvd]] += ['S%02dE%02d' % (seas_no, ep_no)] - - for k, v in cur_ep.items(): + for k, v in cur_ep.iteritems(): k = k.lower() if None is not v: if 'filename' == k: - v = self._get_url_artwork(v) + v = self.config['url_artworkPrefix'] % v else: v = self._clean_data(v) + if k in ep_map_keys: + k = ep_map_keys[k] self._set_item(sid, seas_no, ep_no, k, v) - if self.config['dvdorder']: - num_dvd, num_network = [len(dvd_order[x]) for x in 'dvd', 'network'] - num_all = num_dvd + num_network - if num_all: - self.log('Of %s episodes, %s use the DVD order, and %s use the network aired order' % ( - num_all, num_dvd, num_network)) - for ep_numbers in [', '.join(dvd_order['dvd'][i:i + 5]) for i in xrange(0, num_dvd, 5)]: - self.log('Using DVD order: %s' % ep_numbers) - return True def _name_to_sid(self, name): @@ -893,10 +931,10 @@ class Tvdb: the correct SID. """ if name in self.corrections: - self.log('Correcting %s to %s' % (name, self.corrections[name])) + log().debug('Correcting %s to %s' % (name, self.corrections[name])) return self.corrections[name] else: - self.log('Getting show %s' % name) + log().debug('Getting show %s' % name) selected_series = self._get_series(name) if isinstance(selected_series, dict): selected_series = [selected_series] @@ -926,7 +964,7 @@ class Tvdb: selected_series = self._get_series(key) if isinstance(selected_series, dict): selected_series = [selected_series] - [[self._set_show_data(show['id'], k, v) for k, v in show.items()] for show in selected_series] + [[self._set_show_data(show['id'], k, v) for k, v in show.iteritems()] for show in selected_series] return selected_series def __repr__(self): diff --git a/lib/tvdb_api/tvdb_exceptions.py b/lib/tvdb_api/tvdb_exceptions.py index 3683ef60..e17e2e60 100644 --- a/lib/tvdb_api/tvdb_exceptions.py +++ b/lib/tvdb_api/tvdb_exceptions.py @@ -50,3 +50,8 @@ class tvdb_attributenotfound(tvdb_exception): attribute (such as a episode name) """ pass + +class tvdb_tokenexpired(tvdb_exception): + """token expired or missing thetvdb.com + """ + pass diff --git a/lib/tvdb_api/tvdb_ui.py b/lib/tvdb_api/tvdb_ui.py index 7725802c..b3ffc787 100644 --- a/lib/tvdb_api/tvdb_ui.py +++ b/lib/tvdb_api/tvdb_ui.py @@ -13,14 +13,14 @@ A UI is a callback. A class, it's __init__ function takes two arguments: - log, which is Tvdb's logger instance (which uses the logging module). You can call log.info() log.warning() etc -It must have a method "selectSeries", this is passed a list of dicts, each dict +It must have a method "select_series", this is passed a list of dicts, each dict contains the the keys "name" (human readable show name), and "sid" (the shows ID as on thetvdb.com). For example: [{'name': u'Lost', 'sid': u'73739'}, {'name': u'Lost Universe', 'sid': u'73181'}] -The "selectSeries" method must return the appropriate dict, or it can raise +The "select_series" method must return the appropriate dict, or it can raise tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show cannot be found). @@ -29,7 +29,7 @@ A simple example callback, which returns a random series: >>> import random >>> from tvdb_ui import BaseUI >>> class RandomUI(BaseUI): -... def selectSeries(self, allSeries): +... def select_series(self, allSeries): ... import random ... return random.choice(allSeries) @@ -50,9 +50,11 @@ import warnings from tvdb_exceptions import tvdb_userabort + def log(): return logging.getLogger(__name__) + class BaseUI: """Default non-interactive UI, which auto-selects first results """ @@ -64,8 +66,8 @@ class BaseUI: "The self.log attribute will be removed in the next version") self.log = logging.getLogger(__name__) - def selectSeries(self, allSeries): - return allSeries[0] + def select_series(self, all_series): + return all_series[0] class ConsoleUI(BaseUI): @@ -98,17 +100,17 @@ class ConsoleUI(BaseUI): extra ) - def selectSeries(self, allSeries): - self._displaySeries(allSeries) + def select_series(self, all_series): + self._displaySeries(all_series) - if len(allSeries) == 1: + if len(all_series) == 1: # Single result, return it! print "Automatically selecting only result" - return allSeries[0] + return all_series[0] if self.config['select_first'] is True: print "Automatically returning first search result" - return allSeries[0] + return all_series[0] while True: # return breaks this loop try: @@ -126,7 +128,7 @@ class ConsoleUI(BaseUI): if len(ans.strip()) == 0: # Default option log().debug('Default option, returning first series') - return allSeries[0] + return all_series[0] if ans == "q": log().debug('Got quit command (q)') raise tvdb_userabort("User aborted ('q' quit command)") @@ -139,15 +141,15 @@ class ConsoleUI(BaseUI): print "# q - abort tvnamer" print "# Press return with no input to select first result" elif ans.lower() in ["a", "all"]: - self._displaySeries(allSeries, limit = None) + self._displaySeries(all_series, limit = None) else: log().debug('Unknown keypress %s' % (ans)) else: log().debug('Trying to return ID: %d' % (selected_id)) try: - return allSeries[selected_id] + return all_series[selected_id] except IndexError: log().debug('Invalid show number entered!') print "Invalid number (%s) selected!" - self._displaySeries(allSeries) + self._displaySeries(all_series) diff --git a/lib/tvrage_api/AUTHORS b/lib/tvrage_api/AUTHORS deleted file mode 100644 index 81e6190b..00000000 --- a/lib/tvrage_api/AUTHORS +++ /dev/null @@ -1,9 +0,0 @@ -Original Author: ------------- -* Christian Kreutzer - -Contributors ------------- -* topdeck (http://bitbucket.org/topdeck) -* samueltardieu (http://bitbucket.org/samueltardieu) -* chevox (https://bitbucket.org/chexov) diff --git a/lib/tvrage_api/LICENSE b/lib/tvrage_api/LICENSE deleted file mode 100644 index 87bfbee4..00000000 --- a/lib/tvrage_api/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2009, Christian Kreutzer -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions, and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions, and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the author of this software nor the name of - contributors to this software may be used to endorse or promote products - derived from this software without specific prior written consent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/lib/tvrage_api/setup.py b/lib/tvrage_api/setup.py deleted file mode 100644 index ec5fcaa7..00000000 --- a/lib/tvrage_api/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python - -import os - -from distutils.core import setup -from tvrage import __version__, __author__, __license__ - -setup(name='python-tvrage', - description='python client for the tvrage.com XML API', - long_description = file( - os.path.join(os.path.dirname(__file__),'README.rst')).read(), - license=__license__, - version=__version__, - author=__author__, - author_email='herr.kreutzer@gmail.com', - # url='http://bitbucket.org/ckreutzer/python-tvrage/', - url='https://github.com/ckreutzer/python-tvrage', - packages=['tvrage'], - install_requires = ["BeautifulSoup"], - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python', - 'Operating System :: OS Independent' - ] - ) - diff --git a/lib/tvrage_api/tvrage_api.py b/lib/tvrage_api/tvrage_api.py deleted file mode 100644 index 23a70292..00000000 --- a/lib/tvrage_api/tvrage_api.py +++ /dev/null @@ -1,701 +0,0 @@ -# !/usr/bin/env python2 -# encoding:utf-8 -#author:dbr/Ben (ripped from tvdb:echel0n) -#project:tvrage_api -#license:unlicense (http://unlicense.org/) - -""" -Modified from http://github.com/dbr/tvrage_api -Simple-to-use Python interface to The TVRage's API (tvrage.com) -""" -from functools import wraps -import traceback - -import os -import re -import time -import getpass -import tempfile -import warnings -import logging -import datetime as dt -import requests -import requests.exceptions -import xmltodict -from sickbeard.network_timezones import standardize_network - -try: - import xml.etree.cElementTree as ElementTree -except ImportError: - import xml.etree.ElementTree as ElementTree - -from lib.dateutil.parser import parse -from lib.cachecontrol import CacheControl, caches - -from tvrage_ui import BaseUI -from tvrage_exceptions import (tvrage_error, tvrage_userabort, tvrage_shownotfound, - tvrage_seasonnotfound, tvrage_episodenotfound, tvrage_attributenotfound) - - -def log(): - return logging.getLogger("tvrage_api") - - -def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): - """Retry calling the decorated function using an exponential backoff. - - http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ - original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry - - :param ExceptionToCheck: the exception to check. may be a tuple of - exceptions to check - :type ExceptionToCheck: Exception or tuple - :param tries: number of times to try (not retry) before giving up - :type tries: int - :param delay: initial delay between retries in seconds - :type delay: int - :param backoff: backoff multiplier e.g. value of 2 will double the delay - each retry - :type backoff: int - :param logger: logger to use. If None, print - :type logger: logging.Logger instance - """ - - def deco_retry(f): - - @wraps(f) - def f_retry(*args, **kwargs): - mtries, mdelay = tries, delay - while mtries > 1: - try: - return f(*args, **kwargs) - except ExceptionToCheck, e: - msg = "%s, Retrying in %d seconds..." % (str(e), mdelay) - if logger: - logger.warning(msg) - else: - print msg - time.sleep(mdelay) - mtries -= 1 - mdelay *= backoff - return f(*args, **kwargs) - - return f_retry # true decorator - - return deco_retry - - -class ShowContainer(dict): - """Simple dict that holds a series of Show instances - """ - - def __init__(self): - self._stack = [] - self._lastgc = time.time() - - def __setitem__(self, key, value): - self._stack.append(key) - - #keep only the 100th latest results - if time.time() - self._lastgc > 20: - for o in self._stack[:-100]: - del self[o] - - self._stack = self._stack[-100:] - - self._lastgc = time.time() - - super(ShowContainer, self).__setitem__(key, value) - - -class Show(dict): - """Holds a dict of seasons, and show data. - """ - - def __init__(self): - dict.__init__(self) - self.data = {} - - def __repr__(self): - return "" % ( - self.data.get(u'seriesname', 'instance'), - len(self) - ) - - def __getattr__(self, key): - if key in self: - # Key is an episode, return it - return self[key] - - if key in self.data: - # Non-numeric request is for show-data - return self.data[key] - - raise AttributeError - - def __getitem__(self, key): - if key in self: - # Key is an episode, return it - return dict.__getitem__(self, key) - - if key in self.data: - # Non-numeric request is for show-data - return dict.__getitem__(self.data, key) - - # Data wasn't found, raise appropriate error - if isinstance(key, int) or key.isdigit(): - # Episode number x was not found - raise tvrage_seasonnotfound("Could not find season %s" % (repr(key))) - else: - # If it's not numeric, it must be an attribute name, which - # doesn't exist, so attribute error. - raise tvrage_attributenotfound("Cannot find attribute %s" % (repr(key))) - - def airedOn(self, date): - ret = self.search(str(date), 'firstaired') - if len(ret) == 0: - raise tvrage_episodenotfound("Could not find any episodes that aired on %s" % date) - return ret - - def search(self, term=None, key=None): - """ - Search all episodes in show. Can search all data, or a specific key (for - example, episodename) - - Always returns an array (can be empty). First index contains the first - match, and so on. - - Each array index is an Episode() instance, so doing - search_results[0]['episodename'] will retrieve the episode name of the - first match. - - Search terms are converted to lower case (unicode) strings. - """ - results = [] - for cur_season in self.values(): - searchresult = cur_season.search(term=term, key=key) - if len(searchresult) != 0: - results.extend(searchresult) - - return results - - -class Season(dict): - def __init__(self, show=None): - """The show attribute points to the parent show - """ - self.show = show - - def __repr__(self): - return "" % ( - len(self.keys()) - ) - - def __getattr__(self, episode_number): - if episode_number in self: - return self[episode_number] - raise AttributeError - - def __getitem__(self, episode_number): - if episode_number not in self: - raise tvrage_episodenotfound("Could not find episode %s" % (repr(episode_number))) - else: - return dict.__getitem__(self, episode_number) - - def search(self, term=None, key=None): - """Search all episodes in season, returns a list of matching Episode - instances. - """ - results = [] - for ep in self.values(): - searchresult = ep.search(term=term, key=key) - if searchresult is not None: - results.append( - searchresult - ) - return results - - -class Episode(dict): - def __init__(self, season=None): - """The season attribute points to the parent season - """ - self.season = season - - def __repr__(self): - seasno = int(self.get(u'seasonnumber', 0)) - epno = int(self.get(u'episodenumber', 0)) - epname = self.get(u'episodename') - if epname is not None: - return "" % (seasno, epno, epname) - else: - return "" % (seasno, epno) - - def __getattr__(self, key): - if key in self: - return self[key] - raise AttributeError - - def __getitem__(self, key): - try: - return dict.__getitem__(self, key) - except KeyError: - raise tvrage_attributenotfound("Cannot find attribute %s" % (repr(key))) - - def search(self, term=None, key=None): - """Search episode data for term, if it matches, return the Episode (self). - The key parameter can be used to limit the search to a specific element, - for example, episodename. - - This primarily for use use by Show.search and Season.search. - """ - if term == None: - raise TypeError("must supply string to search for (contents)") - - term = unicode(term).lower() - for cur_key, cur_value in self.items(): - cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower() - if key is not None and cur_key != key: - # Do not search this key - continue - if cur_value.find(unicode(term).lower()) > -1: - return self - - -class TVRage: - """Create easy-to-use interface to name of season/episode name""" - - def __init__(self, - interactive=False, - select_first=False, - debug=False, - cache=True, - banners=False, - actors=False, - custom_ui=None, - language=None, - search_all_languages=False, - apikey=None, - forceConnect=False, - useZip=False, - dvdorder=False, - proxy=None): - - """ - cache (True/False/str/unicode/urllib2 opener): - Retrieved XML are persisted to to disc. If true, stores in - tvrage_api folder under your systems TEMP_DIR, if set to - str/unicode instance it will use this as the cache - location. If False, disables caching. Can also be passed - an arbitrary Python object, which is used as a urllib2 - opener, which should be created by urllib2.build_opener - - forceConnect (bool): - If true it will always try to connect to tvrage.com even if we - recently timed out. By default it will wait one minute before - trying again, and any requests within that one minute window will - return an exception immediately. - """ - - self.shows = ShowContainer() # Holds all Show classes - self.corrections = {} # Holds show-name to show_id mapping - - self.config = {} - - if apikey is not None: - self.config['apikey'] = apikey - else: - self.config['apikey'] = "Uhewg1Rr0o62fvZvUIZt" # tvdb_api's API key - - self.config['debug_enabled'] = debug # show debugging messages - - self.config['custom_ui'] = custom_ui - - self.config['proxy'] = proxy - - if cache is True: - self.config['cache_enabled'] = True - self.config['cache_location'] = self._getTempDir() - elif cache is False: - self.config['cache_enabled'] = False - elif isinstance(cache, basestring): - self.config['cache_enabled'] = True - self.config['cache_location'] = cache - else: - raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache))) - - if self.config['debug_enabled']: - warnings.warn("The debug argument to tvrage_api.__init__ will be removed in the next version. " - "To enable debug messages, use the following code before importing: " - "import logging; logging.basicConfig(level=logging.DEBUG)") - logging.basicConfig(level=logging.DEBUG) - - - # List of language from http://tvrage.com/api/0629B785CE550C8D/languages.xml - # Hard-coded here as it is realtively static, and saves another HTTP request, as - # recommended on http://tvrage.com/wiki/index.php/API:languages.xml - self.config['valid_languages'] = [ - "da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr", - "ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv", "no" - ] - - # tvrage.com should be based around numeric language codes, - # but to link to a series like http://tvrage.com/?tab=series&id=79349&lid=16 - # requires the language ID, thus this mapping is required (mainly - # for usage in tvrage_ui - internally tvrage_api will use the language abbreviations) - self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27, - 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, - 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, - 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30} - - if language is None: - self.config['language'] = 'en' - else: - if language not in self.config['valid_languages']: - raise ValueError("Invalid language %s, options are: %s" % ( - language, self.config['valid_languages'] - )) - else: - self.config['language'] = language - - # The following url_ configs are based of the - # http://tvrage.com/wiki/index.php/Programmers_API - - self.config['base_url'] = "http://services.tvrage.com" - - self.config['url_getSeries'] = u"%(base_url)s/feeds/full_search.php" % self.config - self.config['params_getSeries'] = {"show": ""} - - self.config['url_epInfo'] = u"%(base_url)s/myfeeds/episode_list.php" % self.config - self.config['params_epInfo'] = {"key": self.config['apikey'], "sid": ""} - - self.config['url_seriesInfo'] = u"%(base_url)s/myfeeds/showinfo.php" % self.config - self.config['params_seriesInfo'] = {"key": self.config['apikey'], "sid": ""} - - self.config['url_updtes_all'] = u"%(base_url)s/myfeeds/currentshows.php" % self.config - - def _getTempDir(self): - """Returns the [system temp dir]/tvrage_api-u501 (or - tvrage_api-myuser) - """ - if hasattr(os, 'getuid'): - uid = "u%d" % (os.getuid()) - else: - # For Windows - try: - uid = getpass.getuser() - except ImportError: - return os.path.join(tempfile.gettempdir(), "tvrage_api") - - return os.path.join(tempfile.gettempdir(), "tvrage_api-%s" % (uid)) - - #@retry(tvrage_error) - def _loadUrl(self, url, params=None): - log().debug('Retrieving URL %s' % url) - - session = requests.session() - - if self.config['cache_enabled']: - session = CacheControl(session, cache=caches.FileCache(self.config['cache_location'])) - - if self.config['proxy']: - log().debug('Using proxy for URL: %s' % url) - session.proxies = {'http': self.config['proxy'], 'https': self.config['proxy']} - - session.headers.update({'Accept-Encoding': 'gzip,deflate'}) - - try: - resp = session.get(url.strip(), params=params) - except requests.exceptions.HTTPError, e: - raise tvrage_error('HTTP error %s while loading URL %s' % (e.errno, url)) - except requests.exceptions.ConnectionError, e: - raise tvrage_error('Connection error %s while loading URL %s' % (e.message, url)) - except requests.exceptions.Timeout, e: - raise tvrage_error('Connection timed out %s while loading URL %s' % (e.message, url)) - except Exception: - raise tvrage_error('Unknown exception while loading URL %s: %s' % (url, traceback.format_exc())) - - def remap_keys(path, key, value): - name_map = { - 'showid': 'id', - 'showname': 'seriesname', - 'name': 'seriesname', - 'summary': 'overview', - 'started': 'firstaired', - 'genres': 'genre', - 'airtime': 'airs_time', - 'airday': 'airs_dayofweek', - 'image': 'fanart', - 'epnum': 'absolute_number', - 'title': 'episodename', - 'airdate': 'firstaired', - 'screencap': 'filename', - 'seasonnum': 'episodenumber' - } - - try: - key = name_map[key.lower()] - except (ValueError, TypeError, KeyError): - key = key.lower() - - # clean up value and do type changes - if value: - if isinstance(value, dict): - if key == 'network': - network = value['#text'] - country = value['@country'] - value = standardize_network(network, country) - if key == 'genre': - value = value['genre'] - if not value: - value = [] - if not isinstance(value, list): - value = [value] - value = filter(None, value) - value = '|' + '|'.join(value) + '|' - try: - if key == 'firstaired' and value in '0000-00-00': - new_value = str(dt.date.fromordinal(1)) - new_value = re.sub('([-]0{2})+', '', new_value) - fix_date = parse(new_value, fuzzy=True).date() - value = fix_date.strftime('%Y-%m-%d') - elif key == 'firstaired': - value = parse(value, fuzzy=True).date() - value = value.strftime('%Y-%m-%d') - - #if key == 'airs_time': - # value = parse(value).time() - # value = value.strftime('%I:%M %p') - except: - pass - - return key, value - - if resp.ok: - try: - return xmltodict.parse(resp.content.strip(), postprocessor=remap_keys) - except: - return dict([(u'data', None)]) - - def _getetsrc(self, url, params=None): - """Loads a URL using caching, returns an ElementTree of the source - """ - - try: - src = self._loadUrl(url, params).values()[0] - return src - except: - return [] - - def _setItem(self, sid, seas, ep, attrib, value): - """Creates a new episode, creating Show(), Season() and - Episode()s as required. Called by _getShowData to populate show - - Since the nice-to-use tvrage[1][24]['name] interface - makes it impossible to do tvrage[1][24]['name] = "name" - and still be capable of checking if an episode exists - so we can raise tvrage_shownotfound, we have a slightly - less pretty method of setting items.. but since the API - is supposed to be read-only, this is the best way to - do it! - The problem is that calling tvrage[1][24]['episodename'] = "name" - calls __getitem__ on tvrage[1], there is no way to check if - tvrage.__dict__ should have a key "1" before we auto-create it - """ - if sid not in self.shows: - self.shows[sid] = Show() - if seas not in self.shows[sid]: - self.shows[sid][seas] = Season(show=self.shows[sid]) - if ep not in self.shows[sid][seas]: - self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) - self.shows[sid][seas][ep][attrib] = value - - def _setShowData(self, sid, key, value): - """Sets self.shows[sid] to a new Show instance, or sets the data - """ - if sid not in self.shows: - self.shows[sid] = Show() - - if not isinstance(key, dict or list) and not isinstance(value, dict or list): - self.shows[sid].data[key] = value - - def _cleanData(self, data): - """Cleans up strings returned by tvrage.com - - Issues corrected: - - Replaces & with & - - Trailing whitespace - """ - - if not isinstance(data, dict or list): - data = data.replace(u"&", u"&") - data = data.strip() - - return data - - def search(self, series): - """This searches tvrage.com for the series name - and returns the result list - """ - series = series.encode("utf-8") - log().debug("Searching for show %s" % series) - self.config['params_getSeries']['show'] = series - - try: - seriesFound = self._getetsrc(self.config['url_getSeries'], self.config['params_getSeries']) - if seriesFound: - return seriesFound.values()[0] - except: - pass - - return [] - - def _getSeries(self, series): - """This searches tvrage.com for the series name, - If a custom_ui UI is configured, it uses this to select the correct - series. If not, and interactive == True, ConsoleUI is used, if not - BaseUI is used to select the first result. - """ - allSeries = self.search(series) - if not isinstance(allSeries, list): - allSeries = [allSeries] - - if len(allSeries) == 0: - log().debug('Series result returned zero') - raise tvrage_shownotfound("Show-name search returned zero results (cannot find show on TVRAGE)") - - if self.config['custom_ui'] is not None: - log().debug("Using custom UI %s" % (repr(self.config['custom_ui']))) - CustomUI = self.config['custom_ui'] - ui = CustomUI(config=self.config) - else: - log().debug('Auto-selecting first search result using BaseUI') - ui = BaseUI(config=self.config) - - return ui.selectSeries(allSeries) - - def _getShowData(self, sid, getEpInfo=False): - """Takes a series ID, gets the epInfo URL and parses the TVRAGE - XML file into the shows dict in layout: - shows[series_id][season_number][episode_number] - """ - - # Parse show information - log().debug('Getting all series data for %s' % (sid)) - self.config['params_seriesInfo']['sid'] = sid - seriesInfoEt = self._getetsrc( - self.config['url_seriesInfo'], - self.config['params_seriesInfo'] - ) - - # check and make sure we have data to process and that it contains a series name - if not len(seriesInfoEt) or (isinstance(seriesInfoEt, dict) and 'seriesname' not in seriesInfoEt): - return False - - for k, v in seriesInfoEt.items(): - if v is not None: - v = self._cleanData(v) - - self._setShowData(sid, k, v) - - # series search ends here - if getEpInfo: - # Parse episode data - log().debug('Getting all episodes of %s' % (sid)) - - self.config['params_epInfo']['sid'] = sid - epsEt = self._getetsrc(self.config['url_epInfo'], self.config['params_epInfo']) - if 'episodelist' not in epsEt or 'season' not in epsEt['episodelist']: - return False - - seasons = epsEt['episodelist']['season'] - if not isinstance(seasons, list): - seasons = [seasons] - - for season in seasons: - seas_no = int(season['@no']) - episodes = season['episode'] - if not isinstance(episodes, list): - episodes = [episodes] - - for episode in episodes: - ep_no = int(episode['episodenumber']) - self._setItem(sid, seas_no, ep_no, 'seasonnumber', seas_no) - - for k, v in episode.items(): - try: - k = k.lower() - if v is not None: - if k == 'link': - v = v.rsplit('/', 1)[1] - k = 'id' - v = self._cleanData(v) - - self._setItem(sid, seas_no, ep_no, k, v) - except: - continue - return True - - def _nameToSid(self, name): - """Takes show name, returns the correct series ID (if the show has - already been grabbed), or grabs all episodes and returns - the correct SID. - """ - if name in self.corrections: - log().debug('Correcting %s to %s' % (name, self.corrections[name])) - return self.corrections[name] - else: - log().debug('Getting show %s' % (name)) - selected_series = self._getSeries(name) - if isinstance(selected_series, dict): - selected_series = [selected_series] - sids = list(int(x['id']) for x in selected_series if self._getShowData(int(x['id']))) - self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series)) - return sids - - def __getitem__(self, key): - """Handles tvrage_instance['seriesname'] calls. - The dict index should be the show id - """ - arg = None - if isinstance(key, tuple) and 2 == len(key): - key, arg = key - if not isinstance(arg, bool): - arg = None - - if isinstance(key, (int, long)): - # Item is integer, treat as show id - if key not in self.shows: - self._getShowData(key, (True, arg)[arg is not None]) - return None if key not in self.shows else self.shows[key] - - key = key.lower() - self.config['searchterm'] = key - selected_series = self._getSeries(key) - if isinstance(selected_series, dict): - selected_series = [selected_series] - [[self._setShowData(show['id'], k, v) for k, v in show.items()] for show in selected_series] - return selected_series - #test = self._getSeries(key) - #sids = self._nameToSid(key) - #return list(self.shows[sid] for sid in sids) - - def __repr__(self): - return str(self.shows) - - -def main(): - """Simple example of using tvrage_api - it just - grabs an episode name interactively. - """ - import logging - - logging.basicConfig(level=logging.DEBUG) - - tvrage_instance = TVRage(cache=False) - print tvrage_instance['Lost']['seriesname'] - print tvrage_instance['Lost'][1][4]['episodename'] - - -if __name__ == '__main__': - main() diff --git a/lib/tvrage_api/tvrage_cache.py b/lib/tvrage_api/tvrage_cache.py deleted file mode 100644 index 34c10bf3..00000000 --- a/lib/tvrage_api/tvrage_cache.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python2 -#encoding:utf-8 -#author:dbr/Ben (ripped from tvdb:echel0n) -#project:tvrage_api -#license:unlicense (http://unlicense.org/) - -""" -urllib2 caching handler -Modified from http://code.activestate.com/recipes/491261/ -""" -from __future__ import with_statement - -import os -import time -import errno -import httplib -import urllib2 -import StringIO -from hashlib import md5 -from threading import RLock - -cache_lock = RLock() - -def locked_function(origfunc): - """Decorator to execute function under lock""" - def wrapped(*args, **kwargs): - cache_lock.acquire() - try: - return origfunc(*args, **kwargs) - finally: - cache_lock.release() - return wrapped - -def calculate_cache_path(cache_location, url): - """Checks if [cache_location]/[hash_of_url].headers and .body exist - """ - thumb = md5(url).hexdigest() - header = os.path.join(cache_location, thumb + ".headers") - body = os.path.join(cache_location, thumb + ".body") - return header, body - -def check_cache_time(path, max_age): - """Checks if a file has been created/modified in the [last max_age] seconds. - False means the file is too old (or doesn't exist), True means it is - up-to-date and valid""" - if not os.path.isfile(path): - return False - cache_modified_time = os.stat(path).st_mtime - time_now = time.time() - if cache_modified_time < time_now - max_age: - # Cache is old - return False - else: - return True - -@locked_function -def exists_in_cache(cache_location, url, max_age): - """Returns if header AND body cache file exist (and are up-to-date)""" - hpath, bpath = calculate_cache_path(cache_location, url) - if os.path.exists(hpath) and os.path.exists(bpath): - return( - check_cache_time(hpath, max_age) - and check_cache_time(bpath, max_age) - ) - else: - # File does not exist - return False - -@locked_function -def store_in_cache(cache_location, url, response): - """Tries to store response in cache.""" - hpath, bpath = calculate_cache_path(cache_location, url) - try: - outf = open(hpath, "wb") - headers = str(response.info()) - outf.write(headers) - outf.close() - - outf = open(bpath, "wb") - outf.write(response.read()) - outf.close() - except IOError: - return True - else: - return False - -@locked_function -def delete_from_cache(cache_location, url): - """Deletes a response in cache.""" - hpath, bpath = calculate_cache_path(cache_location, url) - try: - if os.path.exists(hpath): - os.remove(hpath) - if os.path.exists(bpath): - os.remove(bpath) - except IOError: - return True - else: - return False - -class CacheHandler(urllib2.BaseHandler): - """Stores responses in a persistant on-disk cache. - - If a subsequent GET request is made for the same URL, the stored - response is returned, saving time, resources and bandwidth - """ - @locked_function - def __init__(self, cache_location, max_age = 21600): - """The location of the cache directory""" - self.max_age = max_age - self.cache_location = cache_location - if not os.path.exists(self.cache_location): - try: - os.mkdir(self.cache_location) - except OSError, e: - if e.errno == errno.EEXIST and os.path.isdir(self.cache_location): - # File exists, and it's a directory, - # another process beat us to creating this dir, that's OK. - pass - else: - # Our target dir is already a file, or different error, - # relay the error! - raise - - def default_open(self, request): - """Handles GET requests, if the response is cached it returns it - """ - if request.get_method() != "GET": - return None # let the next handler try to handle the request - - if exists_in_cache( - self.cache_location, request.get_full_url(), self.max_age - ): - return CachedResponse( - self.cache_location, - request.get_full_url(), - set_cache_header = True - ) - else: - return None - - def http_response(self, request, response): - """Gets a HTTP response, if it was a GET request and the status code - starts with 2 (200 OK etc) it caches it and returns a CachedResponse - """ - if (request.get_method() == "GET" - and str(response.code).startswith("2") - ): - if 'x-local-cache' not in response.info(): - # Response is not cached - set_cache_header = store_in_cache( - self.cache_location, - request.get_full_url(), - response - ) - else: - set_cache_header = True - - return CachedResponse( - self.cache_location, - request.get_full_url(), - set_cache_header = set_cache_header - ) - else: - return response - -class CachedResponse(StringIO.StringIO): - """An urllib2.response-like object for cached responses. - - To determine if a response is cached or coming directly from - the network, check the x-local-cache header rather than the object type. - """ - - @locked_function - def __init__(self, cache_location, url, set_cache_header=True): - self.cache_location = cache_location - hpath, bpath = calculate_cache_path(cache_location, url) - - StringIO.StringIO.__init__(self, file(bpath, "rb").read()) - - self.url = url - self.code = 200 - self.msg = "OK" - headerbuf = file(hpath, "rb").read() - if set_cache_header: - headerbuf += "x-local-cache: %s\r\n" % (bpath) - self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf)) - - def info(self): - """Returns headers - """ - return self.headers - - def geturl(self): - """Returns original URL - """ - return self.url - - @locked_function - def recache(self): - new_request = urllib2.urlopen(self.url) - set_cache_header = store_in_cache( - self.cache_location, - new_request.url, - new_request - ) - CachedResponse.__init__(self, self.cache_location, self.url, True) - - @locked_function - def delete_cache(self): - delete_from_cache( - self.cache_location, - self.url - ) - - -if __name__ == "__main__": - def main(): - """Quick test/example of CacheHandler""" - opener = urllib2.build_opener(CacheHandler("/tmp/")) - response = opener.open("http://google.com") - print response.headers - print "Response:", response.read() - - response.recache() - print response.headers - print "After recache:", response.read() - - # Test usage in threads - from threading import Thread - class CacheThreadTest(Thread): - lastdata = None - def run(self): - req = opener.open("http://google.com") - newdata = req.read() - if self.lastdata is None: - self.lastdata = newdata - assert self.lastdata == newdata, "Data was not consistent, uhoh" - req.recache() - threads = [CacheThreadTest() for x in range(50)] - print "Starting threads" - [t.start() for t in threads] - print "..done" - print "Joining threads" - [t.join() for t in threads] - print "..done" - main() diff --git a/lib/tvrage_api/tvrage_exceptions.py b/lib/tvrage_api/tvrage_exceptions.py deleted file mode 100644 index 61818c98..00000000 --- a/lib/tvrage_api/tvrage_exceptions.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python2 -#encoding:utf-8 -#author:dbr/Ben (ripped from tvdb:echel0n) -#project:tvrage_api - -#license:unlicense (http://unlicense.org/) - -"""Custom exceptions used or raised by tvrage_api""" - -__all__ = ["tvrage_error", "tvrage_userabort", "tvrage_shownotfound", -"tvrage_seasonnotfound", "tvrage_episodenotfound", "tvrage_attributenotfound"] - -class tvrage_exception(Exception): - """Any exception generated by tvrage_api - """ - pass - -class tvrage_error(tvrage_exception): - """An error with tvrage.com (Cannot connect, for example) - """ - pass - -class tvrage_userabort(tvrage_exception): - """User aborted the interactive selection (via - the q command, ^c etc) - """ - pass - -class tvrage_shownotfound(tvrage_exception): - """Show cannot be found on tvrage.com (non-existant show) - """ - pass - -class tvrage_seasonnotfound(tvrage_exception): - """Season cannot be found on tvrage.com - """ - pass - -class tvrage_episodenotfound(tvrage_exception): - """Episode cannot be found on tvrage.com - """ - pass - -class tvrage_attributenotfound(tvrage_exception): - """Raised if an episode does not have the requested - attribute (such as a episode name) - """ - pass diff --git a/lib/tvrage_api/tvrage_ui.py b/lib/tvrage_api/tvrage_ui.py deleted file mode 100644 index 4cb74673..00000000 --- a/lib/tvrage_api/tvrage_ui.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python2 -#encoding:utf-8 -#author:dbr/Ben (ripped from tvdb:echel0n) -#project:tvrage_api - -#license:unlicense (http://unlicense.org/) - -"""Contains included user interface for TVRage show selection""" - -import logging -import warnings - -def log(): - return logging.getLogger(__name__) - -class BaseUI: - """Default non-interactive UI, which auto-selects first results - """ - def __init__(self, config, log = None): - self.config = config - if log is not None: - warnings.warn("the UI's log parameter is deprecated, instead use\n" - "use import logging; logging.getLogger('ui').info('blah')\n" - "The self.log attribute will be removed in the next version") - self.log = logging.getLogger(__name__) - - def selectSeries(self, allSeries): - return allSeries[0] \ No newline at end of file diff --git a/lib/unidecode/x002.py b/lib/unidecode/x002.py index ea45441e..d7028cdf 100644 --- a/lib/unidecode/x002.py +++ b/lib/unidecode/x002.py @@ -175,7 +175,7 @@ data = ( ']]', # 0xad 'h', # 0xae 'h', # 0xaf -'k', # 0xb0 +'h', # 0xb0 'h', # 0xb1 'j', # 0xb2 'r', # 0xb3 diff --git a/lib/unidecode/x005.py b/lib/unidecode/x005.py index 2913ffff..85d6abbc 100644 --- a/lib/unidecode/x005.py +++ b/lib/unidecode/x005.py @@ -189,7 +189,7 @@ data = ( 'u', # 0xbb '\'', # 0xbc '', # 0xbd -'', # 0xbe +'-', # 0xbe '', # 0xbf '|', # 0xc0 '', # 0xc1 diff --git a/lib/unidecode/x020.py b/lib/unidecode/x020.py index b6494730..bee561b0 100644 --- a/lib/unidecode/x020.py +++ b/lib/unidecode/x020.py @@ -94,7 +94,7 @@ data = ( '[?]', # 0x5c '[?]', # 0x5d '[?]', # 0x5e -'[?]', # 0x5f +' ', # 0x5f '', # 0x60 '[?]', # 0x61 '[?]', # 0x62 @@ -112,7 +112,7 @@ data = ( '', # 0x6e '', # 0x6f '0', # 0x70 -'', # 0x71 +'i', # 0x71 '', # 0x72 '', # 0x73 '4', # 0x74 @@ -143,19 +143,19 @@ data = ( '(', # 0x8d ')', # 0x8e '[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 +'a', # 0x90 +'e', # 0x91 +'o', # 0x92 +'x', # 0x93 '[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c +'h', # 0x95 +'k', # 0x96 +'l', # 0x97 +'m', # 0x98 +'n', # 0x99 +'p', # 0x9a +'s', # 0x9b +'t', # 0x9c '[?]', # 0x9d '[?]', # 0x9e '[?]', # 0x9f diff --git a/lib/unidecode/x021.py b/lib/unidecode/x021.py index 067d9bdc..29f05fd4 100644 --- a/lib/unidecode/x021.py +++ b/lib/unidecode/x021.py @@ -1,33 +1,33 @@ data = ( -'', # 0x00 -'', # 0x01 +' a/c ', # 0x00 +' a/s ', # 0x01 'C', # 0x02 '', # 0x03 '', # 0x04 -'', # 0x05 -'', # 0x06 +' c/o ', # 0x05 +' c/u ', # 0x06 '', # 0x07 '', # 0x08 '', # 0x09 -'', # 0x0a -'', # 0x0b -'', # 0x0c +'g', # 0x0a +'H', # 0x0b +'H', # 0x0c 'H', # 0x0d -'', # 0x0e +'h', # 0x0e '', # 0x0f -'', # 0x10 -'', # 0x11 -'', # 0x12 -'', # 0x13 +'I', # 0x10 +'I', # 0x11 +'L', # 0x12 +'l', # 0x13 '', # 0x14 'N', # 0x15 -'', # 0x16 +'No. ', # 0x16 '', # 0x17 '', # 0x18 'P', # 0x19 'Q', # 0x1a -'', # 0x1b -'', # 0x1c +'R', # 0x1b +'R', # 0x1c 'R', # 0x1d '', # 0x1e '', # 0x1f @@ -39,24 +39,24 @@ data = ( '', # 0x25 '', # 0x26 '', # 0x27 -'', # 0x28 +'Z', # 0x28 '', # 0x29 'K', # 0x2a 'A', # 0x2b -'', # 0x2c -'', # 0x2d +'B', # 0x2c +'C', # 0x2d 'e', # 0x2e 'e', # 0x2f 'E', # 0x30 'F', # 0x31 'F', # 0x32 'M', # 0x33 -'', # 0x34 +'o', # 0x34 '', # 0x35 '', # 0x36 '', # 0x37 '', # 0x38 -'', # 0x39 +'i', # 0x39 '', # 0x3a 'FAX', # 0x3b '', # 0x3c @@ -79,9 +79,9 @@ data = ( '[?]', # 0x4d 'F', # 0x4e '[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 +' 1/7 ', # 0x50 +' 1/9 ', # 0x51 +' 1/10 ', # 0x52 ' 1/3 ', # 0x53 ' 2/3 ', # 0x54 ' 1/5 ', # 0x55 @@ -136,7 +136,7 @@ data = ( '[?]', # 0x86 '[?]', # 0x87 '[?]', # 0x88 -'[?]', # 0x89 +' 0/3 ', # 0x89 '[?]', # 0x8a '[?]', # 0x8b '[?]', # 0x8c diff --git a/lib/unidecode/x024.py b/lib/unidecode/x024.py index 20b3c8f1..231b0ca1 100644 --- a/lib/unidecode/x024.py +++ b/lib/unidecode/x024.py @@ -181,32 +181,32 @@ data = ( '(x)', # 0xb3 '(y)', # 0xb4 '(z)', # 0xb5 -'a', # 0xb6 -'b', # 0xb7 -'c', # 0xb8 -'d', # 0xb9 -'e', # 0xba -'f', # 0xbb -'g', # 0xbc -'h', # 0xbd -'i', # 0xbe -'j', # 0xbf -'k', # 0xc0 -'l', # 0xc1 -'m', # 0xc2 -'n', # 0xc3 -'o', # 0xc4 -'p', # 0xc5 -'q', # 0xc6 -'r', # 0xc7 -'s', # 0xc8 -'t', # 0xc9 -'u', # 0xca -'v', # 0xcb -'w', # 0xcc -'x', # 0xcd -'y', # 0xce -'z', # 0xcf +'A', # 0xb6 +'B', # 0xb7 +'C', # 0xb8 +'D', # 0xb9 +'E', # 0xba +'F', # 0xbb +'G', # 0xbc +'H', # 0xbd +'I', # 0xbe +'J', # 0xbf +'K', # 0xc0 +'L', # 0xc1 +'M', # 0xc2 +'N', # 0xc3 +'O', # 0xc4 +'P', # 0xc5 +'Q', # 0xc6 +'R', # 0xc7 +'S', # 0xc8 +'T', # 0xc9 +'U', # 0xca +'V', # 0xcb +'W', # 0xcc +'X', # 0xcd +'Y', # 0xce +'Z', # 0xcf 'a', # 0xd0 'b', # 0xd1 'c', # 0xd2 @@ -234,24 +234,25 @@ data = ( 'y', # 0xe8 'z', # 0xe9 '0', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +'11', # 0xeb +'12', # 0xec +'13', # 0xed +'14', # 0xee +'15', # 0xef +'16', # 0xf0 +'17', # 0xf1 +'18', # 0xf2 +'19', # 0xf3 +'20', # 0xf4 +'1', # 0xf5 +'2', # 0xf6 +'3', # 0xf7 +'4', # 0xf8 +'5', # 0xf9 +'6', # 0xfa +'7', # 0xfb +'8', # 0xfc +'9', # 0xfd +'10', # 0xfe +'0', # 0xff ) diff --git a/lib/unidecode/x032.py b/lib/unidecode/x032.py index 30282d4a..a0c21d11 100644 --- a/lib/unidecode/x032.py +++ b/lib/unidecode/x032.py @@ -203,10 +203,10 @@ data = ( '10M', # 0xc9 '11M', # 0xca '12M', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +'Hg', # 0xcc +'erg', # 0xcd +'eV', # 0xce +'LTD', # 0xcf 'a', # 0xd0 'i', # 0xd1 'u', # 0xd2 diff --git a/lib/unidecode/x033.py b/lib/unidecode/x033.py index 64eb651a..b9536832 100644 --- a/lib/unidecode/x033.py +++ b/lib/unidecode/x033.py @@ -112,16 +112,16 @@ data = ( '22h', # 0x6e '23h', # 0x6f '24h', # 0x70 -'HPA', # 0x71 +'hPa', # 0x71 'da', # 0x72 'AU', # 0x73 'bar', # 0x74 'oV', # 0x75 'pc', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a +'dm', # 0x77 +'dm^2', # 0x78 +'dm^3', # 0x79 +'IU', # 0x7a 'Heisei', # 0x7b 'Syouwa', # 0x7c 'Taisyou', # 0x7d @@ -162,7 +162,7 @@ data = ( 'cm^2', # 0xa0 'm^2', # 0xa1 'km^2', # 0xa2 -'mm^4', # 0xa3 +'mm^3', # 0xa3 'cm^3', # 0xa4 'm^3', # 0xa5 'km^3', # 0xa6 @@ -254,4 +254,5 @@ data = ( '29d', # 0xfc '30d', # 0xfd '31d', # 0xfe +'gal', # 0xff ) diff --git a/lib/unidecode/x1f1.py b/lib/unidecode/x1f1.py new file mode 100644 index 00000000..ba0481fc --- /dev/null +++ b/lib/unidecode/x1f1.py @@ -0,0 +1,258 @@ +data = ( +'0.', # 0x00 +'0,', # 0x01 +'1,', # 0x02 +'2,', # 0x03 +'3,', # 0x04 +'4,', # 0x05 +'5,', # 0x06 +'6,', # 0x07 +'7,', # 0x08 +'8,', # 0x09 +'9,', # 0x0a +'', # 0x0b +'', # 0x0c +'', # 0x0d +'', # 0x0e +'', # 0x0f +'(A)', # 0x10 +'(B)', # 0x11 +'(C)', # 0x12 +'(D)', # 0x13 +'(E)', # 0x14 +'(F)', # 0x15 +'(G)', # 0x16 +'(H)', # 0x17 +'(I)', # 0x18 +'(J)', # 0x19 +'(K)', # 0x1a +'(L)', # 0x1b +'(M)', # 0x1c +'(N)', # 0x1d +'(O)', # 0x1e +'(P)', # 0x1f +'(Q)', # 0x20 +'(R)', # 0x21 +'(S)', # 0x22 +'(T)', # 0x23 +'(U)', # 0x24 +'(V)', # 0x25 +'(W)', # 0x26 +'(X)', # 0x27 +'(Y)', # 0x28 +'(Z)', # 0x29 +'', # 0x2a +'', # 0x2b +'', # 0x2c +'', # 0x2d +'', # 0x2e +'', # 0x2f +'', # 0x30 +'', # 0x31 +'', # 0x32 +'', # 0x33 +'', # 0x34 +'', # 0x35 +'', # 0x36 +'', # 0x37 +'', # 0x38 +'', # 0x39 +'', # 0x3a +'', # 0x3b +'', # 0x3c +'', # 0x3d +'', # 0x3e +'', # 0x3f +'', # 0x40 +'', # 0x41 +'', # 0x42 +'', # 0x43 +'', # 0x44 +'', # 0x45 +'', # 0x46 +'', # 0x47 +'', # 0x48 +'', # 0x49 +'', # 0x4a +'', # 0x4b +'', # 0x4c +'', # 0x4d +'', # 0x4e +'', # 0x4f +'', # 0x50 +'', # 0x51 +'', # 0x52 +'', # 0x53 +'', # 0x54 +'', # 0x55 +'', # 0x56 +'', # 0x57 +'', # 0x58 +'', # 0x59 +'', # 0x5a +'', # 0x5b +'', # 0x5c +'', # 0x5d +'', # 0x5e +'', # 0x5f +'', # 0x60 +'', # 0x61 +'', # 0x62 +'', # 0x63 +'', # 0x64 +'', # 0x65 +'', # 0x66 +'', # 0x67 +'', # 0x68 +'', # 0x69 +'', # 0x6a +'', # 0x6b +'', # 0x6c +'', # 0x6d +'', # 0x6e +'', # 0x6f +'', # 0x70 +'', # 0x71 +'', # 0x72 +'', # 0x73 +'', # 0x74 +'', # 0x75 +'', # 0x76 +'', # 0x77 +'', # 0x78 +'', # 0x79 +'', # 0x7a +'', # 0x7b +'', # 0x7c +'', # 0x7d +'', # 0x7e +'', # 0x7f +'', # 0x80 +'', # 0x81 +'', # 0x82 +'', # 0x83 +'', # 0x84 +'', # 0x85 +'', # 0x86 +'', # 0x87 +'', # 0x88 +'', # 0x89 +'', # 0x8a +'', # 0x8b +'', # 0x8c +'', # 0x8d +'', # 0x8e +'', # 0x8f +'', # 0x90 +'', # 0x91 +'', # 0x92 +'', # 0x93 +'', # 0x94 +'', # 0x95 +'', # 0x96 +'', # 0x97 +'', # 0x98 +'', # 0x99 +'', # 0x9a +'', # 0x9b +'', # 0x9c +'', # 0x9d +'', # 0x9e +'', # 0x9f +'', # 0xa0 +'', # 0xa1 +'', # 0xa2 +'', # 0xa3 +'', # 0xa4 +'', # 0xa5 +'', # 0xa6 +'', # 0xa7 +'', # 0xa8 +'', # 0xa9 +'', # 0xaa +'', # 0xab +'', # 0xac +'', # 0xad +'', # 0xae +'', # 0xaf +'', # 0xb0 +'', # 0xb1 +'', # 0xb2 +'', # 0xb3 +'', # 0xb4 +'', # 0xb5 +'', # 0xb6 +'', # 0xb7 +'', # 0xb8 +'', # 0xb9 +'', # 0xba +'', # 0xbb +'', # 0xbc +'', # 0xbd +'', # 0xbe +'', # 0xbf +'', # 0xc0 +'', # 0xc1 +'', # 0xc2 +'', # 0xc3 +'', # 0xc4 +'', # 0xc5 +'', # 0xc6 +'', # 0xc7 +'', # 0xc8 +'', # 0xc9 +'', # 0xca +'', # 0xcb +'', # 0xcc +'', # 0xcd +'', # 0xce +'', # 0xcf +'', # 0xd0 +'', # 0xd1 +'', # 0xd2 +'', # 0xd3 +'', # 0xd4 +'', # 0xd5 +'', # 0xd6 +'', # 0xd7 +'', # 0xd8 +'', # 0xd9 +'', # 0xda +'', # 0xdb +'', # 0xdc +'', # 0xdd +'', # 0xde +'', # 0xdf +'', # 0xe0 +'', # 0xe1 +'', # 0xe2 +'', # 0xe3 +'', # 0xe4 +'', # 0xe5 +'', # 0xe6 +'', # 0xe7 +'', # 0xe8 +'', # 0xe9 +'', # 0xea +'', # 0xeb +'', # 0xec +'', # 0xed +'', # 0xee +'', # 0xef +'', # 0xf0 +'', # 0xf1 +'', # 0xf2 +'', # 0xf3 +'', # 0xf4 +'', # 0xf5 +'', # 0xf6 +'', # 0xf7 +'', # 0xf8 +'', # 0xf9 +'', # 0xfa +'', # 0xfb +'', # 0xfc +'', # 0xfd +'', # 0xfe +'', # 0xff +) diff --git a/lib/webencodings/__init__.py b/lib/webencodings/__init__.py new file mode 100644 index 00000000..03d5d357 --- /dev/null +++ b/lib/webencodings/__init__.py @@ -0,0 +1,342 @@ +# coding: utf8 +""" + + webencodings + ~~~~~~~~~~~~ + + This is a Python implementation of the `WHATWG Encoding standard + `. See README for details. + + :copyright: Copyright 2012 by Simon Sapin + :license: BSD, see LICENSE for details. + +""" + +from __future__ import unicode_literals + +import codecs + +from .labels import LABELS + + +VERSION = '0.5' + + +# Some names in Encoding are not valid Python aliases. Remap these. +PYTHON_NAMES = { + 'iso-8859-8-i': 'iso-8859-8', + 'x-mac-cyrillic': 'mac-cyrillic', + 'macintosh': 'mac-roman', + 'windows-874': 'cp874'} + +CACHE = {} + + +def ascii_lower(string): + r"""Transform (only) ASCII letters to lower case: A-Z is mapped to a-z. + + :param string: An Unicode string. + :returns: A new Unicode string. + + This is used for `ASCII case-insensitive + `_ + matching of encoding labels. + The same matching is also used, among other things, + for `CSS keywords `_. + + This is different from the :meth:`~py:str.lower` method of Unicode strings + which also affect non-ASCII characters, + sometimes mapping them into the ASCII range: + + >>> keyword = u'Bac\N{KELVIN SIGN}ground' + >>> assert keyword.lower() == u'background' + >>> assert ascii_lower(keyword) != keyword.lower() + >>> assert ascii_lower(keyword) == u'bac\N{KELVIN SIGN}ground' + + """ + # This turns out to be faster than unicode.translate() + return string.encode('utf8').lower().decode('utf8') + + +def lookup(label): + """ + Look for an encoding by its label. + This is the spec’s `get an encoding + `_ algorithm. + Supported labels are listed there. + + :param label: A string. + :returns: + An :class:`Encoding` object, or :obj:`None` for an unknown label. + + """ + # Only strip ASCII whitespace: U+0009, U+000A, U+000C, U+000D, and U+0020. + label = ascii_lower(label.strip('\t\n\f\r ')) + name = LABELS.get(label) + if name is None: + return None + encoding = CACHE.get(name) + if encoding is None: + if name == 'x-user-defined': + from .x_user_defined import codec_info + else: + python_name = PYTHON_NAMES.get(name, name) + # Any python_name value that gets to here should be valid. + codec_info = codecs.lookup(python_name) + encoding = Encoding(name, codec_info) + CACHE[name] = encoding + return encoding + + +def _get_encoding(encoding_or_label): + """ + Accept either an encoding object or label. + + :param encoding: An :class:`Encoding` object or a label string. + :returns: An :class:`Encoding` object. + :raises: :exc:`~exceptions.LookupError` for an unknown label. + + """ + if hasattr(encoding_or_label, 'codec_info'): + return encoding_or_label + + encoding = lookup(encoding_or_label) + if encoding is None: + raise LookupError('Unknown encoding label: %r' % encoding_or_label) + return encoding + + +class Encoding(object): + """Reresents a character encoding such as UTF-8, + that can be used for decoding or encoding. + + .. attribute:: name + + Canonical name of the encoding + + .. attribute:: codec_info + + The actual implementation of the encoding, + a stdlib :class:`~codecs.CodecInfo` object. + See :func:`codecs.register`. + + """ + def __init__(self, name, codec_info): + self.name = name + self.codec_info = codec_info + + def __repr__(self): + return '' % self.name + + +#: The UTF-8 encoding. Should be used for new content and formats. +UTF8 = lookup('utf-8') + +_UTF16LE = lookup('utf-16le') +_UTF16BE = lookup('utf-16be') + + +def decode(input, fallback_encoding, errors='replace'): + """ + Decode a single string. + + :param input: A byte string + :param fallback_encoding: + An :class:`Encoding` object or a label string. + The encoding to use if :obj:`input` does note have a BOM. + :param errors: Type of error handling. See :func:`codecs.register`. + :raises: :exc:`~exceptions.LookupError` for an unknown encoding label. + :return: + A ``(output, encoding)`` tuple of an Unicode string + and an :obj:`Encoding`. + + """ + # Fail early if `encoding` is an invalid label. + fallback_encoding = _get_encoding(fallback_encoding) + bom_encoding, input = _detect_bom(input) + encoding = bom_encoding or fallback_encoding + return encoding.codec_info.decode(input, errors)[0], encoding + + +def _detect_bom(input): + """Return (bom_encoding, input), with any BOM removed from the input.""" + if input.startswith(b'\xFF\xFE'): + return _UTF16LE, input[2:] + if input.startswith(b'\xFE\xFF'): + return _UTF16BE, input[2:] + if input.startswith(b'\xEF\xBB\xBF'): + return UTF8, input[3:] + return None, input + + +def encode(input, encoding=UTF8, errors='strict'): + """ + Encode a single string. + + :param input: An Unicode string. + :param encoding: An :class:`Encoding` object or a label string. + :param errors: Type of error handling. See :func:`codecs.register`. + :raises: :exc:`~exceptions.LookupError` for an unknown encoding label. + :return: A byte string. + + """ + return _get_encoding(encoding).codec_info.encode(input, errors)[0] + + +def iter_decode(input, fallback_encoding, errors='replace'): + """ + "Pull"-based decoder. + + :param input: + An iterable of byte strings. + + The input is first consumed just enough to determine the encoding + based on the precense of a BOM, + then consumed on demand when the return value is. + :param fallback_encoding: + An :class:`Encoding` object or a label string. + The encoding to use if :obj:`input` does note have a BOM. + :param errors: Type of error handling. See :func:`codecs.register`. + :raises: :exc:`~exceptions.LookupError` for an unknown encoding label. + :returns: + An ``(output, encoding)`` tuple. + :obj:`output` is an iterable of Unicode strings, + :obj:`encoding` is the :obj:`Encoding` that is being used. + + """ + + decoder = IncrementalDecoder(fallback_encoding, errors) + generator = _iter_decode_generator(input, decoder) + encoding = next(generator) + return generator, encoding + + +def _iter_decode_generator(input, decoder): + """Return a generator that first yields the :obj:`Encoding`, + then yields output chukns as Unicode strings. + + """ + decode = decoder.decode + input = iter(input) + for chunck in input: + output = decode(chunck) + if output: + assert decoder.encoding is not None + yield decoder.encoding + yield output + break + else: + # Input exhausted without determining the encoding + output = decode(b'', final=True) + assert decoder.encoding is not None + yield decoder.encoding + if output: + yield output + return + + for chunck in input: + output = decode(chunck) + if output: + yield output + output = decode(b'', final=True) + if output: + yield output + + +def iter_encode(input, encoding=UTF8, errors='strict'): + """ + “Pull”-based encoder. + + :param input: An iterable of Unicode strings. + :param encoding: An :class:`Encoding` object or a label string. + :param errors: Type of error handling. See :func:`codecs.register`. + :raises: :exc:`~exceptions.LookupError` for an unknown encoding label. + :returns: An iterable of byte strings. + + """ + # Fail early if `encoding` is an invalid label. + encode = IncrementalEncoder(encoding, errors).encode + return _iter_encode_generator(input, encode) + + +def _iter_encode_generator(input, encode): + for chunck in input: + output = encode(chunck) + if output: + yield output + output = encode('', final=True) + if output: + yield output + + +class IncrementalDecoder(object): + """ + “Push”-based decoder. + + :param fallback_encoding: + An :class:`Encoding` object or a label string. + The encoding to use if :obj:`input` does note have a BOM. + :param errors: Type of error handling. See :func:`codecs.register`. + :raises: :exc:`~exceptions.LookupError` for an unknown encoding label. + + """ + def __init__(self, fallback_encoding, errors='replace'): + # Fail early if `encoding` is an invalid label. + self._fallback_encoding = _get_encoding(fallback_encoding) + self._errors = errors + self._buffer = b'' + self._decoder = None + #: The actual :class:`Encoding` that is being used, + #: or :obj:`None` if that is not determined yet. + #: (Ie. if there is not enough input yet to determine + #: if there is a BOM.) + self.encoding = None # Not known yet. + + def decode(self, input, final=False): + """Decode one chunk of the input. + + :param input: A byte string. + :param final: + Indicate that no more input is available. + Must be :obj:`True` if this is the last call. + :returns: An Unicode string. + + """ + decoder = self._decoder + if decoder is not None: + return decoder(input, final) + + input = self._buffer + input + encoding, input = _detect_bom(input) + if encoding is None: + if len(input) < 3 and not final: # Not enough data yet. + self._buffer = input + return '' + else: # No BOM + encoding = self._fallback_encoding + decoder = encoding.codec_info.incrementaldecoder(self._errors).decode + self._decoder = decoder + self.encoding = encoding + return decoder(input, final) + + +class IncrementalEncoder(object): + """ + “Push”-based encoder. + + :param encoding: An :class:`Encoding` object or a label string. + :param errors: Type of error handling. See :func:`codecs.register`. + :raises: :exc:`~exceptions.LookupError` for an unknown encoding label. + + .. method:: encode(input, final=False) + + :param input: An Unicode string. + :param final: + Indicate that no more input is available. + Must be :obj:`True` if this is the last call. + :returns: A byte string. + + """ + def __init__(self, encoding=UTF8, errors='strict'): + encoding = _get_encoding(encoding) + self.encode = encoding.codec_info.incrementalencoder(errors).encode diff --git a/lib/webencodings/labels.py b/lib/webencodings/labels.py new file mode 100644 index 00000000..29cbf91e --- /dev/null +++ b/lib/webencodings/labels.py @@ -0,0 +1,231 @@ +""" + + webencodings.labels + ~~~~~~~~~~~~~~~~~~~ + + Map encoding labels to their name. + + :copyright: Copyright 2012 by Simon Sapin + :license: BSD, see LICENSE for details. + +""" + +# XXX Do not edit! +# This file is automatically generated by mklabels.py + +LABELS = { + 'unicode-1-1-utf-8': 'utf-8', + 'utf-8': 'utf-8', + 'utf8': 'utf-8', + '866': 'ibm866', + 'cp866': 'ibm866', + 'csibm866': 'ibm866', + 'ibm866': 'ibm866', + 'csisolatin2': 'iso-8859-2', + 'iso-8859-2': 'iso-8859-2', + 'iso-ir-101': 'iso-8859-2', + 'iso8859-2': 'iso-8859-2', + 'iso88592': 'iso-8859-2', + 'iso_8859-2': 'iso-8859-2', + 'iso_8859-2:1987': 'iso-8859-2', + 'l2': 'iso-8859-2', + 'latin2': 'iso-8859-2', + 'csisolatin3': 'iso-8859-3', + 'iso-8859-3': 'iso-8859-3', + 'iso-ir-109': 'iso-8859-3', + 'iso8859-3': 'iso-8859-3', + 'iso88593': 'iso-8859-3', + 'iso_8859-3': 'iso-8859-3', + 'iso_8859-3:1988': 'iso-8859-3', + 'l3': 'iso-8859-3', + 'latin3': 'iso-8859-3', + 'csisolatin4': 'iso-8859-4', + 'iso-8859-4': 'iso-8859-4', + 'iso-ir-110': 'iso-8859-4', + 'iso8859-4': 'iso-8859-4', + 'iso88594': 'iso-8859-4', + 'iso_8859-4': 'iso-8859-4', + 'iso_8859-4:1988': 'iso-8859-4', + 'l4': 'iso-8859-4', + 'latin4': 'iso-8859-4', + 'csisolatincyrillic': 'iso-8859-5', + 'cyrillic': 'iso-8859-5', + 'iso-8859-5': 'iso-8859-5', + 'iso-ir-144': 'iso-8859-5', + 'iso8859-5': 'iso-8859-5', + 'iso88595': 'iso-8859-5', + 'iso_8859-5': 'iso-8859-5', + 'iso_8859-5:1988': 'iso-8859-5', + 'arabic': 'iso-8859-6', + 'asmo-708': 'iso-8859-6', + 'csiso88596e': 'iso-8859-6', + 'csiso88596i': 'iso-8859-6', + 'csisolatinarabic': 'iso-8859-6', + 'ecma-114': 'iso-8859-6', + 'iso-8859-6': 'iso-8859-6', + 'iso-8859-6-e': 'iso-8859-6', + 'iso-8859-6-i': 'iso-8859-6', + 'iso-ir-127': 'iso-8859-6', + 'iso8859-6': 'iso-8859-6', + 'iso88596': 'iso-8859-6', + 'iso_8859-6': 'iso-8859-6', + 'iso_8859-6:1987': 'iso-8859-6', + 'csisolatingreek': 'iso-8859-7', + 'ecma-118': 'iso-8859-7', + 'elot_928': 'iso-8859-7', + 'greek': 'iso-8859-7', + 'greek8': 'iso-8859-7', + 'iso-8859-7': 'iso-8859-7', + 'iso-ir-126': 'iso-8859-7', + 'iso8859-7': 'iso-8859-7', + 'iso88597': 'iso-8859-7', + 'iso_8859-7': 'iso-8859-7', + 'iso_8859-7:1987': 'iso-8859-7', + 'sun_eu_greek': 'iso-8859-7', + 'csiso88598e': 'iso-8859-8', + 'csisolatinhebrew': 'iso-8859-8', + 'hebrew': 'iso-8859-8', + 'iso-8859-8': 'iso-8859-8', + 'iso-8859-8-e': 'iso-8859-8', + 'iso-ir-138': 'iso-8859-8', + 'iso8859-8': 'iso-8859-8', + 'iso88598': 'iso-8859-8', + 'iso_8859-8': 'iso-8859-8', + 'iso_8859-8:1988': 'iso-8859-8', + 'visual': 'iso-8859-8', + 'csiso88598i': 'iso-8859-8-i', + 'iso-8859-8-i': 'iso-8859-8-i', + 'logical': 'iso-8859-8-i', + 'csisolatin6': 'iso-8859-10', + 'iso-8859-10': 'iso-8859-10', + 'iso-ir-157': 'iso-8859-10', + 'iso8859-10': 'iso-8859-10', + 'iso885910': 'iso-8859-10', + 'l6': 'iso-8859-10', + 'latin6': 'iso-8859-10', + 'iso-8859-13': 'iso-8859-13', + 'iso8859-13': 'iso-8859-13', + 'iso885913': 'iso-8859-13', + 'iso-8859-14': 'iso-8859-14', + 'iso8859-14': 'iso-8859-14', + 'iso885914': 'iso-8859-14', + 'csisolatin9': 'iso-8859-15', + 'iso-8859-15': 'iso-8859-15', + 'iso8859-15': 'iso-8859-15', + 'iso885915': 'iso-8859-15', + 'iso_8859-15': 'iso-8859-15', + 'l9': 'iso-8859-15', + 'iso-8859-16': 'iso-8859-16', + 'cskoi8r': 'koi8-r', + 'koi': 'koi8-r', + 'koi8': 'koi8-r', + 'koi8-r': 'koi8-r', + 'koi8_r': 'koi8-r', + 'koi8-u': 'koi8-u', + 'csmacintosh': 'macintosh', + 'mac': 'macintosh', + 'macintosh': 'macintosh', + 'x-mac-roman': 'macintosh', + 'dos-874': 'windows-874', + 'iso-8859-11': 'windows-874', + 'iso8859-11': 'windows-874', + 'iso885911': 'windows-874', + 'tis-620': 'windows-874', + 'windows-874': 'windows-874', + 'cp1250': 'windows-1250', + 'windows-1250': 'windows-1250', + 'x-cp1250': 'windows-1250', + 'cp1251': 'windows-1251', + 'windows-1251': 'windows-1251', + 'x-cp1251': 'windows-1251', + 'ansi_x3.4-1968': 'windows-1252', + 'ascii': 'windows-1252', + 'cp1252': 'windows-1252', + 'cp819': 'windows-1252', + 'csisolatin1': 'windows-1252', + 'ibm819': 'windows-1252', + 'iso-8859-1': 'windows-1252', + 'iso-ir-100': 'windows-1252', + 'iso8859-1': 'windows-1252', + 'iso88591': 'windows-1252', + 'iso_8859-1': 'windows-1252', + 'iso_8859-1:1987': 'windows-1252', + 'l1': 'windows-1252', + 'latin1': 'windows-1252', + 'us-ascii': 'windows-1252', + 'windows-1252': 'windows-1252', + 'x-cp1252': 'windows-1252', + 'cp1253': 'windows-1253', + 'windows-1253': 'windows-1253', + 'x-cp1253': 'windows-1253', + 'cp1254': 'windows-1254', + 'csisolatin5': 'windows-1254', + 'iso-8859-9': 'windows-1254', + 'iso-ir-148': 'windows-1254', + 'iso8859-9': 'windows-1254', + 'iso88599': 'windows-1254', + 'iso_8859-9': 'windows-1254', + 'iso_8859-9:1989': 'windows-1254', + 'l5': 'windows-1254', + 'latin5': 'windows-1254', + 'windows-1254': 'windows-1254', + 'x-cp1254': 'windows-1254', + 'cp1255': 'windows-1255', + 'windows-1255': 'windows-1255', + 'x-cp1255': 'windows-1255', + 'cp1256': 'windows-1256', + 'windows-1256': 'windows-1256', + 'x-cp1256': 'windows-1256', + 'cp1257': 'windows-1257', + 'windows-1257': 'windows-1257', + 'x-cp1257': 'windows-1257', + 'cp1258': 'windows-1258', + 'windows-1258': 'windows-1258', + 'x-cp1258': 'windows-1258', + 'x-mac-cyrillic': 'x-mac-cyrillic', + 'x-mac-ukrainian': 'x-mac-cyrillic', + 'chinese': 'gbk', + 'csgb2312': 'gbk', + 'csiso58gb231280': 'gbk', + 'gb2312': 'gbk', + 'gb_2312': 'gbk', + 'gb_2312-80': 'gbk', + 'gbk': 'gbk', + 'iso-ir-58': 'gbk', + 'x-gbk': 'gbk', + 'gb18030': 'gb18030', + 'hz-gb-2312': 'hz-gb-2312', + 'big5': 'big5', + 'big5-hkscs': 'big5', + 'cn-big5': 'big5', + 'csbig5': 'big5', + 'x-x-big5': 'big5', + 'cseucpkdfmtjapanese': 'euc-jp', + 'euc-jp': 'euc-jp', + 'x-euc-jp': 'euc-jp', + 'csiso2022jp': 'iso-2022-jp', + 'iso-2022-jp': 'iso-2022-jp', + 'csshiftjis': 'shift_jis', + 'ms_kanji': 'shift_jis', + 'shift-jis': 'shift_jis', + 'shift_jis': 'shift_jis', + 'sjis': 'shift_jis', + 'windows-31j': 'shift_jis', + 'x-sjis': 'shift_jis', + 'cseuckr': 'euc-kr', + 'csksc56011987': 'euc-kr', + 'euc-kr': 'euc-kr', + 'iso-ir-149': 'euc-kr', + 'korean': 'euc-kr', + 'ks_c_5601-1987': 'euc-kr', + 'ks_c_5601-1989': 'euc-kr', + 'ksc5601': 'euc-kr', + 'ksc_5601': 'euc-kr', + 'windows-949': 'euc-kr', + 'csiso2022kr': 'iso-2022-kr', + 'iso-2022-kr': 'iso-2022-kr', + 'utf-16be': 'utf-16be', + 'utf-16': 'utf-16le', + 'utf-16le': 'utf-16le', + 'x-user-defined': 'x-user-defined', +} diff --git a/lib/webencodings/mklabels.py b/lib/webencodings/mklabels.py new file mode 100644 index 00000000..295dc928 --- /dev/null +++ b/lib/webencodings/mklabels.py @@ -0,0 +1,59 @@ +""" + + webencodings.mklabels + ~~~~~~~~~~~~~~~~~~~~~ + + Regenarate the webencodings.labels module. + + :copyright: Copyright 2012 by Simon Sapin + :license: BSD, see LICENSE for details. + +""" + +import json +try: + from urllib import urlopen +except ImportError: + from urllib.request import urlopen + + +def assert_lower(string): + assert string == string.lower() + return string + + +def generate(url): + parts = ['''\ +""" + + webencodings.labels + ~~~~~~~~~~~~~~~~~~~ + + Map encoding labels to their name. + + :copyright: Copyright 2012 by Simon Sapin + :license: BSD, see LICENSE for details. + +""" + +# XXX Do not edit! +# This file is automatically generated by mklabels.py + +LABELS = { +'''] + labels = [ + (repr(assert_lower(label)).lstrip('u'), + repr(encoding['name']).lstrip('u')) + for category in json.loads(urlopen(url).read().decode('ascii')) + for encoding in category['encodings'] + for label in encoding['labels']] + max_len = max(len(label) for label, name in labels) + parts.extend( + ' %s:%s %s,\n' % (label, ' ' * (max_len - len(label)), name) + for label, name in labels) + parts.append('}') + return ''.join(parts) + + +if __name__ == '__main__': + print(generate('http://encoding.spec.whatwg.org/encodings.json')) diff --git a/lib/webencodings/tests.py b/lib/webencodings/tests.py new file mode 100644 index 00000000..b8c5653e --- /dev/null +++ b/lib/webencodings/tests.py @@ -0,0 +1,153 @@ +# coding: utf8 +""" + + webencodings.tests + ~~~~~~~~~~~~~~~~~~ + + A basic test suite for Encoding. + + :copyright: Copyright 2012 by Simon Sapin + :license: BSD, see LICENSE for details. + +""" + +from __future__ import unicode_literals + +from . import (lookup, LABELS, decode, encode, iter_decode, iter_encode, + IncrementalDecoder, IncrementalEncoder, UTF8) + + +def assert_raises(exception, function, *args, **kwargs): + try: + function(*args, **kwargs) + except exception: + return + else: # pragma: no cover + raise AssertionError('Did not raise %s.' % exception) + + +def test_labels(): + assert lookup('utf-8').name == 'utf-8' + assert lookup('Utf-8').name == 'utf-8' + assert lookup('UTF-8').name == 'utf-8' + assert lookup('utf8').name == 'utf-8' + assert lookup('utf8').name == 'utf-8' + assert lookup('utf8 ').name == 'utf-8' + assert lookup(' \r\nutf8\t').name == 'utf-8' + assert lookup('u8') is None # Python label. + assert lookup('utf-8 ') is None # Non-ASCII white space. + + assert lookup('US-ASCII').name == 'windows-1252' + assert lookup('iso-8859-1').name == 'windows-1252' + assert lookup('latin1').name == 'windows-1252' + assert lookup('LATIN1').name == 'windows-1252' + assert lookup('latin-1') is None + assert lookup('LATİN1') is None # ASCII-only case insensitivity. + + +def test_all_labels(): + for label in LABELS: + assert decode(b'', label) == ('', lookup(label)) + assert encode('', label) == b'' + for repeat in [0, 1, 12]: + output, _ = iter_decode([b''] * repeat, label) + assert list(output) == [] + assert list(iter_encode([''] * repeat, label)) == [] + decoder = IncrementalDecoder(label) + assert decoder.decode(b'') == '' + assert decoder.decode(b'', final=True) == '' + encoder = IncrementalEncoder(label) + assert encoder.encode('') == b'' + assert encoder.encode('', final=True) == b'' + # All encoding names are valid labels too: + for name in set(LABELS.values()): + assert lookup(name).name == name + + +def test_invalid_label(): + assert_raises(LookupError, decode, b'\xEF\xBB\xBF\xc3\xa9', 'invalid') + assert_raises(LookupError, encode, 'é', 'invalid') + assert_raises(LookupError, iter_decode, [], 'invalid') + assert_raises(LookupError, iter_encode, [], 'invalid') + assert_raises(LookupError, IncrementalDecoder, 'invalid') + assert_raises(LookupError, IncrementalEncoder, 'invalid') + + +def test_decode(): + assert decode(b'\x80', 'latin1') == ('€', lookup('latin1')) + assert decode(b'\x80', lookup('latin1')) == ('€', lookup('latin1')) + assert decode(b'\xc3\xa9', 'utf8') == ('é', lookup('utf8')) + assert decode(b'\xc3\xa9', UTF8) == ('é', lookup('utf8')) + assert decode(b'\xc3\xa9', 'ascii') == ('é', lookup('ascii')) + assert decode(b'\xEF\xBB\xBF\xc3\xa9', 'ascii') == ('é', lookup('utf8')) # UTF-8 with BOM + + assert decode(b'\xFE\xFF\x00\xe9', 'ascii') == ('é', lookup('utf-16be')) # UTF-16-BE with BOM + assert decode(b'\xFF\xFE\xe9\x00', 'ascii') == ('é', lookup('utf-16le')) # UTF-16-LE with BOM + assert decode(b'\xFE\xFF\xe9\x00', 'ascii') == ('\ue900', lookup('utf-16be')) + assert decode(b'\xFF\xFE\x00\xe9', 'ascii') == ('\ue900', lookup('utf-16le')) + + assert decode(b'\x00\xe9', 'UTF-16BE') == ('é', lookup('utf-16be')) + assert decode(b'\xe9\x00', 'UTF-16LE') == ('é', lookup('utf-16le')) + assert decode(b'\xe9\x00', 'UTF-16') == ('é', lookup('utf-16le')) + + assert decode(b'\xe9\x00', 'UTF-16BE') == ('\ue900', lookup('utf-16be')) + assert decode(b'\x00\xe9', 'UTF-16LE') == ('\ue900', lookup('utf-16le')) + assert decode(b'\x00\xe9', 'UTF-16') == ('\ue900', lookup('utf-16le')) + + +def test_encode(): + assert encode('é', 'latin1') == b'\xe9' + assert encode('é', 'utf8') == b'\xc3\xa9' + assert encode('é', 'utf8') == b'\xc3\xa9' + assert encode('é', 'utf-16') == b'\xe9\x00' + assert encode('é', 'utf-16le') == b'\xe9\x00' + assert encode('é', 'utf-16be') == b'\x00\xe9' + + +def test_iter_decode(): + def iter_decode_to_string(input, fallback_encoding): + output, _encoding = iter_decode(input, fallback_encoding) + return ''.join(output) + assert iter_decode_to_string([], 'latin1') == '' + assert iter_decode_to_string([b''], 'latin1') == '' + assert iter_decode_to_string([b'\xe9'], 'latin1') == 'é' + assert iter_decode_to_string([b'hello'], 'latin1') == 'hello' + assert iter_decode_to_string([b'he', b'llo'], 'latin1') == 'hello' + assert iter_decode_to_string([b'hell', b'o'], 'latin1') == 'hello' + assert iter_decode_to_string([b'\xc3\xa9'], 'latin1') == 'é' + assert iter_decode_to_string([b'\xEF\xBB\xBF\xc3\xa9'], 'latin1') == 'é' + assert iter_decode_to_string([ + b'\xEF\xBB\xBF', b'\xc3', b'\xa9'], 'latin1') == 'é' + assert iter_decode_to_string([ + b'\xEF\xBB\xBF', b'a', b'\xc3'], 'latin1') == 'a\uFFFD' + assert iter_decode_to_string([ + b'', b'\xEF', b'', b'', b'\xBB\xBF\xc3', b'\xa9'], 'latin1') == 'é' + assert iter_decode_to_string([b'\xEF\xBB\xBF'], 'latin1') == '' + assert iter_decode_to_string([b'\xEF\xBB'], 'latin1') == 'ï»' + assert iter_decode_to_string([b'\xFE\xFF\x00\xe9'], 'latin1') == 'é' + assert iter_decode_to_string([b'\xFF\xFE\xe9\x00'], 'latin1') == 'é' + assert iter_decode_to_string([ + b'', b'\xFF', b'', b'', b'\xFE\xe9', b'\x00'], 'latin1') == 'é' + assert iter_decode_to_string([ + b'', b'h\xe9', b'llo'], 'x-user-defined') == 'h\uF7E9llo' + + +def test_iter_encode(): + assert b''.join(iter_encode([], 'latin1')) == b'' + assert b''.join(iter_encode([''], 'latin1')) == b'' + assert b''.join(iter_encode(['é'], 'latin1')) == b'\xe9' + assert b''.join(iter_encode(['', 'é', '', ''], 'latin1')) == b'\xe9' + assert b''.join(iter_encode(['', 'é', '', ''], 'utf-16')) == b'\xe9\x00' + assert b''.join(iter_encode(['', 'é', '', ''], 'utf-16le')) == b'\xe9\x00' + assert b''.join(iter_encode(['', 'é', '', ''], 'utf-16be')) == b'\x00\xe9' + assert b''.join(iter_encode([ + '', 'h\uF7E9', '', 'llo'], 'x-user-defined')) == b'h\xe9llo' + + +def test_x_user_defined(): + encoded = b'2,\x0c\x0b\x1aO\xd9#\xcb\x0f\xc9\xbbt\xcf\xa8\xca' + decoded = '2,\x0c\x0b\x1aO\uf7d9#\uf7cb\x0f\uf7c9\uf7bbt\uf7cf\uf7a8\uf7ca' + encoded = b'aa' + decoded = 'aa' + assert decode(encoded, 'x-user-defined') == (decoded, lookup('x-user-defined')) + assert encode(decoded, 'x-user-defined') == encoded diff --git a/lib/webencodings/x_user_defined.py b/lib/webencodings/x_user_defined.py new file mode 100644 index 00000000..f0daa11a --- /dev/null +++ b/lib/webencodings/x_user_defined.py @@ -0,0 +1,325 @@ +# coding: utf8 +""" + + webencodings.x_user_defined + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + An implementation of the x-user-defined encoding. + + :copyright: Copyright 2012 by Simon Sapin + :license: BSD, see LICENSE for details. + +""" + +from __future__ import unicode_literals + +import codecs + + +### Codec APIs + +class Codec(codecs.Codec): + + def encode(self, input, errors='strict'): + return codecs.charmap_encode(input, errors, encoding_table) + + def decode(self, input, errors='strict'): + return codecs.charmap_decode(input, errors, decoding_table) + + +class IncrementalEncoder(codecs.IncrementalEncoder): + def encode(self, input, final=False): + return codecs.charmap_encode(input, self.errors, encoding_table)[0] + + +class IncrementalDecoder(codecs.IncrementalDecoder): + def decode(self, input, final=False): + return codecs.charmap_decode(input, self.errors, decoding_table)[0] + + +class StreamWriter(Codec, codecs.StreamWriter): + pass + + +class StreamReader(Codec, codecs.StreamReader): + pass + + +### encodings module API + +codec_info = codecs.CodecInfo( + name='x-user-defined', + encode=Codec().encode, + decode=Codec().decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamreader=StreamReader, + streamwriter=StreamWriter, +) + + +### Decoding Table + +# Python 3: +# for c in range(256): print(' %r' % chr(c if c < 128 else c + 0xF700)) +decoding_table = ( + '\x00' + '\x01' + '\x02' + '\x03' + '\x04' + '\x05' + '\x06' + '\x07' + '\x08' + '\t' + '\n' + '\x0b' + '\x0c' + '\r' + '\x0e' + '\x0f' + '\x10' + '\x11' + '\x12' + '\x13' + '\x14' + '\x15' + '\x16' + '\x17' + '\x18' + '\x19' + '\x1a' + '\x1b' + '\x1c' + '\x1d' + '\x1e' + '\x1f' + ' ' + '!' + '"' + '#' + '$' + '%' + '&' + "'" + '(' + ')' + '*' + '+' + ',' + '-' + '.' + '/' + '0' + '1' + '2' + '3' + '4' + '5' + '6' + '7' + '8' + '9' + ':' + ';' + '<' + '=' + '>' + '?' + '@' + 'A' + 'B' + 'C' + 'D' + 'E' + 'F' + 'G' + 'H' + 'I' + 'J' + 'K' + 'L' + 'M' + 'N' + 'O' + 'P' + 'Q' + 'R' + 'S' + 'T' + 'U' + 'V' + 'W' + 'X' + 'Y' + 'Z' + '[' + '\\' + ']' + '^' + '_' + '`' + 'a' + 'b' + 'c' + 'd' + 'e' + 'f' + 'g' + 'h' + 'i' + 'j' + 'k' + 'l' + 'm' + 'n' + 'o' + 'p' + 'q' + 'r' + 's' + 't' + 'u' + 'v' + 'w' + 'x' + 'y' + 'z' + '{' + '|' + '}' + '~' + '\x7f' + '\uf780' + '\uf781' + '\uf782' + '\uf783' + '\uf784' + '\uf785' + '\uf786' + '\uf787' + '\uf788' + '\uf789' + '\uf78a' + '\uf78b' + '\uf78c' + '\uf78d' + '\uf78e' + '\uf78f' + '\uf790' + '\uf791' + '\uf792' + '\uf793' + '\uf794' + '\uf795' + '\uf796' + '\uf797' + '\uf798' + '\uf799' + '\uf79a' + '\uf79b' + '\uf79c' + '\uf79d' + '\uf79e' + '\uf79f' + '\uf7a0' + '\uf7a1' + '\uf7a2' + '\uf7a3' + '\uf7a4' + '\uf7a5' + '\uf7a6' + '\uf7a7' + '\uf7a8' + '\uf7a9' + '\uf7aa' + '\uf7ab' + '\uf7ac' + '\uf7ad' + '\uf7ae' + '\uf7af' + '\uf7b0' + '\uf7b1' + '\uf7b2' + '\uf7b3' + '\uf7b4' + '\uf7b5' + '\uf7b6' + '\uf7b7' + '\uf7b8' + '\uf7b9' + '\uf7ba' + '\uf7bb' + '\uf7bc' + '\uf7bd' + '\uf7be' + '\uf7bf' + '\uf7c0' + '\uf7c1' + '\uf7c2' + '\uf7c3' + '\uf7c4' + '\uf7c5' + '\uf7c6' + '\uf7c7' + '\uf7c8' + '\uf7c9' + '\uf7ca' + '\uf7cb' + '\uf7cc' + '\uf7cd' + '\uf7ce' + '\uf7cf' + '\uf7d0' + '\uf7d1' + '\uf7d2' + '\uf7d3' + '\uf7d4' + '\uf7d5' + '\uf7d6' + '\uf7d7' + '\uf7d8' + '\uf7d9' + '\uf7da' + '\uf7db' + '\uf7dc' + '\uf7dd' + '\uf7de' + '\uf7df' + '\uf7e0' + '\uf7e1' + '\uf7e2' + '\uf7e3' + '\uf7e4' + '\uf7e5' + '\uf7e6' + '\uf7e7' + '\uf7e8' + '\uf7e9' + '\uf7ea' + '\uf7eb' + '\uf7ec' + '\uf7ed' + '\uf7ee' + '\uf7ef' + '\uf7f0' + '\uf7f1' + '\uf7f2' + '\uf7f3' + '\uf7f4' + '\uf7f5' + '\uf7f6' + '\uf7f7' + '\uf7f8' + '\uf7f9' + '\uf7fa' + '\uf7fb' + '\uf7fc' + '\uf7fd' + '\uf7fe' + '\uf7ff' +) + +### Encoding table +encoding_table = codecs.charmap_build(decoding_table) diff --git a/lib/xmltodict.py b/lib/xmltodict.py index 310130b0..e0639852 100644 --- a/lib/xmltodict.py +++ b/lib/xmltodict.py @@ -1,7 +1,10 @@ #!/usr/bin/env python "Makes working with XML feel like you are working with JSON" -from xml.parsers import expat +try: + from defusedexpat import pyexpat as expat +except ImportError: + from xml.parsers import expat from xml.sax.saxutils import XMLGenerator from xml.sax.xmlreader import AttributesImpl try: # pragma no cover @@ -29,7 +32,7 @@ except NameError: # pragma no cover _unicode = str __author__ = 'Martin Blech' -__version__ = '0.9.2' +__version__ = '0.10.2' __license__ = 'MIT' @@ -51,7 +54,7 @@ class _DictSAXHandler(object): strip_whitespace=True, namespace_separator=':', namespaces=None, - force_list=()): + force_list=None): self.path = [] self.stack = [] self.data = [] @@ -68,6 +71,7 @@ class _DictSAXHandler(object): self.strip_whitespace = strip_whitespace self.namespace_separator = namespace_separator self.namespaces = namespaces + self.namespace_declarations = OrderedDict() self.force_list = force_list def _build_name(self, full_name): @@ -88,16 +92,29 @@ class _DictSAXHandler(object): return attrs return self.dict_constructor(zip(attrs[0::2], attrs[1::2])) + def startNamespaceDecl(self, prefix, uri): + self.namespace_declarations[prefix or ''] = uri + def startElement(self, full_name, attrs): name = self._build_name(full_name) attrs = self._attrs_to_dict(attrs) + if attrs and self.namespace_declarations: + attrs['xmlns'] = self.namespace_declarations + self.namespace_declarations = OrderedDict() self.path.append((name, attrs or None)) if len(self.path) > self.item_depth: self.stack.append((self.item, self.data)) if self.xml_attribs: - attrs = self.dict_constructor( - (self.attr_prefix+self._build_name(key), value) - for (key, value) in attrs.items()) + attr_entries = [] + for key, value in attrs.items(): + key = self.attr_prefix+self._build_name(key) + if self.postprocessor: + entry = self.postprocessor(self.path, key, value) + else: + entry = (key, value) + if entry: + attr_entries.append(entry) + attrs = self.dict_constructor(attr_entries) else: attrs = None self.item = attrs or None @@ -155,12 +172,20 @@ class _DictSAXHandler(object): else: item[key] = [value, data] except KeyError: - if key in self.force_list: + if self._should_force_list(key, data): item[key] = [data] else: item[key] = data return item + def _should_force_list(self, key, value): + if not self.force_list: + return False + try: + return key in self.force_list + except TypeError: + return self.force_list(self.path[:-1], key, value) + def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, namespace_separator=':', **kwargs): @@ -199,7 +224,7 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, Streaming example:: >>> def handle(path, item): - ... print 'path:%s item:%s' % (path, item) + ... print('path:%s item:%s' % (path, item)) ... return True ... >>> xmltodict.parse(\"\"\" @@ -261,6 +286,10 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, 'interfaces': {'interface': [ {'name': 'em0', 'ip_address': '10.0.0.1' } ] } } } + + `force_list` can also be a callable that receives `path`, `key` and + `value`. This is helpful in cases where the logic that decides whether + a list should be forced is more complex. """ handler = _DictSAXHandler(namespace_separator=namespace_separator, **kwargs) @@ -279,6 +308,7 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, except AttributeError: # Jython's expat does not support ordered_attributes pass + parser.StartNamespaceDeclHandler = handler.startNamespaceDecl parser.StartElementHandler = handler.startElement parser.EndElementHandler = handler.endElement parser.CharacterDataHandler = handler.characters @@ -290,6 +320,21 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, return handler.item +def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'): + if not namespaces: + return name + try: + ns, name = name.rsplit(ns_sep, 1) + except ValueError: + pass + else: + ns_res = namespaces.get(ns.strip(attr_prefix)) + name = '{0}{1}{2}{3}'.format( + attr_prefix if ns.startswith(attr_prefix) else '', + ns_res, ns_sep, name) if ns_res else name + return name + + def _emit(key, value, content_handler, attr_prefix='@', cdata_key='#text', @@ -298,7 +343,10 @@ def _emit(key, value, content_handler, pretty=False, newl='\n', indent='\t', + namespace_separator=':', + namespaces=None, full_document=True): + key = _process_namespace(key, namespaces, namespace_separator, attr_prefix) if preprocessor is not None: result = preprocessor(key, value) if result is None: @@ -325,6 +373,15 @@ def _emit(key, value, content_handler, cdata = iv continue if ik.startswith(attr_prefix): + ik = _process_namespace(ik, namespaces, namespace_separator, + attr_prefix) + if ik == '@xmlns' and isinstance(iv, dict): + for k, v in iv.items(): + attr = 'xmlns{0}'.format(':{0}'.format(k) if k else '') + attrs[attr] = _unicode(v) + continue + if not isinstance(iv, _unicode): + iv = _unicode(iv) attrs[ik[len(attr_prefix):]] = iv continue children.append((ik, iv)) @@ -336,7 +393,8 @@ def _emit(key, value, content_handler, for child_key, child_value in children: _emit(child_key, child_value, content_handler, attr_prefix, cdata_key, depth+1, preprocessor, - pretty, newl, indent) + pretty, newl, indent, namespaces=namespaces, + namespace_separator=namespace_separator) if cdata is not None: content_handler.characters(cdata) if pretty and children: @@ -347,6 +405,7 @@ def _emit(key, value, content_handler, def unparse(input_dict, output=None, encoding='utf-8', full_document=True, + short_empty_elements=False, **kwargs): """Emit an XML document for the given `input_dict` (reverse of `parse`). @@ -368,7 +427,10 @@ def unparse(input_dict, output=None, encoding='utf-8', full_document=True, if output is None: output = StringIO() must_return = True - content_handler = XMLGenerator(output, encoding) + if short_empty_elements: + content_handler = XMLGenerator(output, encoding, True) + else: + content_handler = XMLGenerator(output, encoding) if full_document: content_handler.startDocument() for key, value in input_dict.items(): @@ -387,16 +449,23 @@ def unparse(input_dict, output=None, encoding='utf-8', full_document=True, if __name__ == '__main__': # pragma: no cover import sys import marshal + try: + stdin = sys.stdin.buffer + stdout = sys.stdout.buffer + except AttributeError: + stdin = sys.stdin + stdout = sys.stdout (item_depth,) = sys.argv[1:] item_depth = int(item_depth) + def handle_item(path, item): - marshal.dump((path, item), sys.stdout) + marshal.dump((path, item), stdout) return True try: - root = parse(sys.stdin, + root = parse(stdin, item_depth=item_depth, item_callback=handle_item, dict_constructor=dict) diff --git a/readme.md b/readme.md index 151d8872..28b9c29c 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,8 @@
-
SickGear
+
SickGear
**SickGear**, a usenet and bittorrent PVR
-*Please note you should know how to use git and setup basic requirements in order to run this software.* +_Please note you should know how to use git and setup basic requirements in order to run this software._ SickGear provides management of TV shows and/or Anime, it can detect new episodes, link to downloader apps, and more. SickGear is a proud descendant of Sick Beard and is humbled to have been endorsed by one of its former lead developers. @@ -10,7 +10,7 @@ Why SickGear? * SickGear maintains perfect uptime with the longest track record of being stable, reliable and trusted to work * SickGear delivers quality from active development with a wealth of options on a dark or light themed interface * [Migrating](https://github.com/SickGear/SickGear/wiki/Install-SickGear-%5B0%5D-Migrate) to a hassle free and feature rich set up is super simple - + ## Features include * Stable, quality assured testing and development cycle * Innovations that inspire imitators @@ -33,9 +33,9 @@ Why SickGear? * Processing nzb/torrents with your downloader application at your chosen qualities * Subtitle management * Notification - * System notifiers (i.e. Kodi, Emby, Plex, XBMC) - * Device notifiers (i.e. Growl, Prowl, Notify My Android) - * Social (i.e. Twitter, E-mail) + * Home Theater/NAS (Emby, Kodi, Plex, Syno, Tivo, and more) + * Social notifiers (Trakt, Slack, Gitter, Discord, E-mail, and more) + * Device notifiers (Boxcar2, Notify My Android, Growl, Prowl, and more) * Server friendly with minimal number of calls (e.g. one request per chosen snatch, not per result) * Can recommend trendy and/or personally tailored shows from Trakt, IMDb, AniDB * Automated alternative show names and episode numbering from XEM @@ -80,10 +80,10 @@ Some of our innovative features; ## Available versions - + - + @@ -114,8 +114,10 @@ Thanks also, to unsung heroes that added source providers; Idan Gutman, Daniel H Finally, a massive thanks to all those that remain in the shadows, the quiet ones who welcome folk to special places, we salute you for your hospitality and for tirelessly keeping up operations. ## Community -* IRC: `irc.freenode.net` channel `#SickGear` - Support is available, but you should understand the basics of your Linux or Windows OS. If you don't understand basics like locating a db file, not running as root, or things like setting file permissions, then SickGear might not be for you. +* web based (most likely one on one with a dev) +*  (`irc.freenode.net` channel `#SickGear`) + +Although support is available, you should understand the basics of your Linux or Windows OS. If you don't understand basics like locating a db file, not running as root, or things like setting file permissions, then SickGear might not be for you. --- Enjoy SickGear - stability, reliability, assured. diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 14a102cc..0e9b9ec8 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -59,7 +59,7 @@ CFG = None CONFIG_FILE = None # This is the version of the config we EXPECT to find -CONFIG_VERSION = 14 +CONFIG_VERSION = 16 # Default encryption version (0 for None) ENCRYPTION_VERSION = 0 @@ -86,10 +86,17 @@ subtitlesFinderScheduler = None # traktCheckerScheduler = None background_mapping_task = None +provider_ping_thread_pool = {} + showList = None UPDATE_SHOWS_ON_START = False SHOW_UPDATE_HOUR = 3 +# non ui settings +REMOVE_FILENAME_CHARS = None +IMPORT_DEFAULT_CHECKED_SHOWS = None +# /non ui settings + providerList = [] newznabProviderList = [] torrentRssProviderList = [] @@ -120,6 +127,7 @@ WEB_USERNAME = None WEB_PASSWORD = None WEB_HOST = None WEB_IPV6 = None +WEB_IPV64 = None HANDLE_REVERSE_PROXY = False PROXY_SETTING = None @@ -159,6 +167,8 @@ METADATA_TIVO = None METADATA_MEDE8ER = None METADATA_KODI = None +RESULTS_SORTBY = None + QUALITY_DEFAULT = None STATUS_DEFAULT = None WANTED_BEGIN_DEFAULT = None @@ -199,6 +209,7 @@ TORRENT_METHOD = None TORRENT_DIR = None DOWNLOAD_PROPERS = False CHECK_PROPERS_INTERVAL = None +PROPERS_WEBDL_ONEGRP = True ALLOW_HIGH_PRIORITY = False NEWZNAB_DATA = '' @@ -225,7 +236,6 @@ SEARCH_UNAIRED = False UNAIRED_RECENT_SEARCH_ONLY = True ADD_SHOWS_WO_DIR = False -REMOVE_FILENAME_CHARS = None CREATE_MISSING_SHOW_DIRS = False RENAME_EPISODES = False AIRDATE_EPISODES = False @@ -239,12 +249,6 @@ TV_DOWNLOAD_DIR = None UNPACK = False SKIP_REMOVED_FILES = False -SAB_USERNAME = None -SAB_PASSWORD = None -SAB_APIKEY = None -SAB_CATEGORY = None -SAB_HOST = '' - NZBGET_USERNAME = None NZBGET_PASSWORD = None NZBGET_CATEGORY = None @@ -252,6 +256,12 @@ NZBGET_HOST = None NZBGET_USE_HTTPS = False NZBGET_PRIORITY = 100 +SAB_USERNAME = None +SAB_PASSWORD = None +SAB_APIKEY = None +SAB_CATEGORY = None +SAB_HOST = '' + TORRENT_USERNAME = None TORRENT_PASSWORD = None TORRENT_HOST = '' @@ -279,6 +289,16 @@ KODI_HOST = '' KODI_USERNAME = None KODI_PASSWORD = None +USE_PLEX = False +PLEX_NOTIFY_ONSNATCH = False +PLEX_NOTIFY_ONDOWNLOAD = False +PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = False +PLEX_UPDATE_LIBRARY = False +PLEX_SERVER_HOST = None +PLEX_HOST = None +PLEX_USERNAME = None +PLEX_PASSWORD = None + USE_XBMC = False XBMC_ALWAYS_ON = True XBMC_NOTIFY_ONSNATCH = False @@ -291,15 +311,52 @@ XBMC_HOST = '' XBMC_USERNAME = None XBMC_PASSWORD = None -USE_PLEX = False -PLEX_NOTIFY_ONSNATCH = False -PLEX_NOTIFY_ONDOWNLOAD = False -PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = False -PLEX_UPDATE_LIBRARY = False -PLEX_SERVER_HOST = None -PLEX_HOST = None -PLEX_USERNAME = None -PLEX_PASSWORD = None +USE_NMJ = False +NMJ_HOST = None +NMJ_DATABASE = None +NMJ_MOUNT = None + +USE_NMJv2 = False +NMJv2_HOST = None +NMJv2_DATABASE = None +NMJv2_DBLOC = None + +USE_SYNOINDEX = False +SYNOINDEX_UPDATE_LIBRARY = True + +USE_SYNOLOGYNOTIFIER = False +SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = False +SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = False +SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = False + +USE_PYTIVO = False +PYTIVO_HOST = '' +PYTIVO_SHARE_NAME = '' +PYTIVO_TIVO_NAME = '' + +USE_BOXCAR2 = False +BOXCAR2_NOTIFY_ONSNATCH = False +BOXCAR2_NOTIFY_ONDOWNLOAD = False +BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD = False +BOXCAR2_ACCESSTOKEN = None +BOXCAR2_SOUND = None + +USE_PUSHBULLET = False +PUSHBULLET_NOTIFY_ONSNATCH = False +PUSHBULLET_NOTIFY_ONDOWNLOAD = False +PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = False +PUSHBULLET_ACCESS_TOKEN = None +PUSHBULLET_DEVICE_IDEN = None + +USE_PUSHOVER = False +PUSHOVER_NOTIFY_ONSNATCH = False +PUSHOVER_NOTIFY_ONDOWNLOAD = False +PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = False +PUSHOVER_USERKEY = None +PUSHOVER_APIKEY = None +PUSHOVER_PRIORITY = 0 +PUSHOVER_DEVICE = None +PUSHOVER_SOUND = None USE_GROWL = False GROWL_NOTIFY_ONSNATCH = False @@ -315,59 +372,23 @@ PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = False PROWL_API = None PROWL_PRIORITY = 0 -USE_TWITTER = False -TWITTER_NOTIFY_ONSNATCH = False -TWITTER_NOTIFY_ONDOWNLOAD = False -TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = False -TWITTER_USERNAME = None -TWITTER_PASSWORD = None -TWITTER_PREFIX = None - -USE_BOXCAR2 = False -BOXCAR2_NOTIFY_ONSNATCH = False -BOXCAR2_NOTIFY_ONDOWNLOAD = False -BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD = False -BOXCAR2_ACCESSTOKEN = None -BOXCAR2_SOUND = None - -USE_PUSHOVER = False -PUSHOVER_NOTIFY_ONSNATCH = False -PUSHOVER_NOTIFY_ONDOWNLOAD = False -PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = False -PUSHOVER_USERKEY = None -PUSHOVER_APIKEY = None -PUSHOVER_PRIORITY = 0 -PUSHOVER_DEVICE = None -PUSHOVER_SOUND = None +USE_NMA = False +NMA_NOTIFY_ONSNATCH = False +NMA_NOTIFY_ONDOWNLOAD = False +NMA_NOTIFY_ONSUBTITLEDOWNLOAD = False +NMA_API = None +NMA_PRIORITY = 0 USE_LIBNOTIFY = False LIBNOTIFY_NOTIFY_ONSNATCH = False LIBNOTIFY_NOTIFY_ONDOWNLOAD = False LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = False -USE_NMJ = False -NMJ_HOST = None -NMJ_DATABASE = None -NMJ_MOUNT = None - -USE_ANIDB = False -ANIDB_USERNAME = None -ANIDB_PASSWORD = None -ANIDB_USE_MYLIST = False -ADBA_CONNECTION = None -ANIME_TREAT_AS_HDTV = False - -USE_SYNOINDEX = False - -USE_NMJv2 = False -NMJv2_HOST = None -NMJv2_DATABASE = None -NMJv2_DBLOC = None - -USE_SYNOLOGYNOTIFIER = False -SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = False -SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = False -SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = False +USE_PUSHALOT = False +PUSHALOT_NOTIFY_ONSNATCH = False +PUSHALOT_NOTIFY_ONDOWNLOAD = False +PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = False +PUSHALOT_AUTHORIZATIONTOKEN = None USE_TRAKT = False TRAKT_REMOVE_WATCHLIST = False @@ -379,34 +400,40 @@ TRAKT_SYNC = False TRAKT_DEFAULT_INDEXER = None TRAKT_UPDATE_COLLECTION = {} -USE_PYTIVO = False -PYTIVO_NOTIFY_ONSNATCH = False -PYTIVO_NOTIFY_ONDOWNLOAD = False -PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = False -PYTIVO_UPDATE_LIBRARY = False -PYTIVO_HOST = '' -PYTIVO_SHARE_NAME = '' -PYTIVO_TIVO_NAME = '' +USE_SLACK = False +SLACK_NOTIFY_ONSNATCH = False +SLACK_NOTIFY_ONDOWNLOAD = False +SLACK_NOTIFY_ONSUBTITLEDOWNLOAD = False +SLACK_CHANNEL = None +SLACK_AS_AUTHED = False +SLACK_BOT_NAME = None +SLACK_ICON_URL = None +SLACK_ACCESS_TOKEN = None -USE_NMA = False -NMA_NOTIFY_ONSNATCH = False -NMA_NOTIFY_ONDOWNLOAD = False -NMA_NOTIFY_ONSUBTITLEDOWNLOAD = False -NMA_API = None -NMA_PRIORITY = 0 +USE_DISCORDAPP = False +DISCORDAPP_NOTIFY_ONSNATCH = False +DISCORDAPP_NOTIFY_ONDOWNLOAD = False +DISCORDAPP_NOTIFY_ONSUBTITLEDOWNLOAD = False +DISCORDAPP_AS_AUTHED = False +DISCORDAPP_USERNAME = None +DISCORDAPP_ICON_URL = None +DISCORDAPP_AS_TTS = None +DISCORDAPP_ACCESS_TOKEN = None -USE_PUSHALOT = False -PUSHALOT_NOTIFY_ONSNATCH = False -PUSHALOT_NOTIFY_ONDOWNLOAD = False -PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = False -PUSHALOT_AUTHORIZATIONTOKEN = None +USE_GITTER = False +GITTER_NOTIFY_ONSNATCH = False +GITTER_NOTIFY_ONDOWNLOAD = False +GITTER_NOTIFY_ONSUBTITLEDOWNLOAD = False +GITTER_ROOM = None +GITTER_ACCESS_TOKEN = None -USE_PUSHBULLET = False -PUSHBULLET_NOTIFY_ONSNATCH = False -PUSHBULLET_NOTIFY_ONDOWNLOAD = False -PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = False -PUSHBULLET_ACCESS_TOKEN = None -PUSHBULLET_DEVICE_IDEN = None +USE_TWITTER = False +TWITTER_NOTIFY_ONSNATCH = False +TWITTER_NOTIFY_ONDOWNLOAD = False +TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = False +TWITTER_USERNAME = None +TWITTER_PASSWORD = None +TWITTER_PREFIX = None USE_EMAIL = False EMAIL_OLD_SUBJECTS = None @@ -421,6 +448,13 @@ EMAIL_PASSWORD = None EMAIL_FROM = None EMAIL_LIST = None +USE_ANIDB = False +ANIDB_USERNAME = None +ANIDB_PASSWORD = None +ANIDB_USE_MYLIST = False +ADBA_CONNECTION = None +ANIME_TREAT_AS_HDTV = False + GUI_NAME = None DEFAULT_HOME = None FANART_LIMIT = None @@ -444,6 +478,7 @@ EPISODE_VIEW_DISPLAY_PAUSED = False EPISODE_VIEW_POSTERS = True EPISODE_VIEW_MISSED_RANGE = None HISTORY_LAYOUT = None +BROWSELIST_HIDDEN = [] FUZZY_DATING = False TRIM_ZERO = False @@ -499,6 +534,8 @@ else: TRAKT_PIN_URL = 'https://trakt.tv/pin/6314' TRAKT_BASE_URL = 'https://api.trakt.tv/' +THETVDB_V2_API_TOKEN = {'token': None, 'datetime': datetime.datetime.fromordinal(1)} + COOKIE_SECRET = base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes) CACHE_IMAGE_URL_LIST = classes.ImageUrlList() @@ -517,13 +554,16 @@ def initialize(console_logging=True): # Misc global __INITIALIZED__, showList, providerList, newznabProviderList, torrentRssProviderList, \ WEB_HOST, WEB_ROOT, ACTUAL_CACHE_DIR, CACHE_DIR, ZONEINFO_DIR, ADD_SHOWS_WO_DIR, CREATE_MISSING_SHOW_DIRS, \ - RECENTSEARCH_STARTUP, NAMING_FORCE_FOLDERS, SOCKET_TIMEOUT, DEBUG, INDEXER_DEFAULT, REMOVE_FILENAME_CHARS, \ - CONFIG_FILE + RECENTSEARCH_STARTUP, NAMING_FORCE_FOLDERS, SOCKET_TIMEOUT, DEBUG, INDEXER_DEFAULT, CONFIG_FILE, \ + REMOVE_FILENAME_CHARS, IMPORT_DEFAULT_CHECKED_SHOWS # Schedulers # global traktCheckerScheduler global recentSearchScheduler, backlogSearchScheduler, showUpdateScheduler, \ versionCheckScheduler, showQueueScheduler, searchQueueScheduler, \ - properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, background_mapping_task + properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, background_mapping_task, \ + provider_ping_thread_pool + # Add Show Search + global RESULTS_SORTBY # Add Show Defaults global STATUS_DEFAULT, QUALITY_DEFAULT, SHOW_TAG_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, \ WANTED_BEGIN_DEFAULT, WANTED_LATEST_DEFAULT, SCENE_DEFAULT, ANIME_DEFAULT @@ -535,7 +575,7 @@ def initialize(console_logging=True): EPISODE_VIEW_MISSED_RANGE, EPISODE_VIEW_POSTERS, FANART_PANEL, FANART_RATINGS, \ EPISODE_VIEW_VIEWMODE, EPISODE_VIEW_BACKGROUND, EPISODE_VIEW_BACKGROUND_TRANSLUCENT, \ DISPLAY_SHOW_VIEWMODE, DISPLAY_SHOW_BACKGROUND, DISPLAY_SHOW_BACKGROUND_TRANSLUCENT, \ - DISPLAY_SHOW_VIEWART, DISPLAY_SHOW_MINIMUM, DISPLAY_SHOW_SPECIALS, HISTORY_LAYOUT + DISPLAY_SHOW_VIEWART, DISPLAY_SHOW_MINIMUM, DISPLAY_SHOW_SPECIALS, HISTORY_LAYOUT, BROWSELIST_HIDDEN # Gen Config/Misc global LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, \ TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, ACTUAL_LOG_DIR, LOG_DIR, INDEXER_TIMEOUT, ROOT_DIRS, \ @@ -545,12 +585,12 @@ def initialize(console_logging=True): HOME_SEARCH_FOCUS, USE_IMDB_INFO, IMDB_ACCOUNTS, SORT_ARTICLE, FUZZY_DATING, TRIM_ZERO, \ DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, TIMEZONE_DISPLAY, \ WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEY, WEB_PORT, WEB_LOG, \ - ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, HANDLE_REVERSE_PROXY + ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, WEB_IPV64, HANDLE_REVERSE_PROXY # Gen Config/Advanced global BRANCH, CUR_COMMIT_BRANCH, GIT_REMOTE, CUR_COMMIT_HASH, GIT_PATH, CPU_PRESET, ANON_REDIRECT, \ ENCRYPTION_VERSION, PROXY_SETTING, PROXY_INDEXERS, FILE_LOGGING_PRESET # Search Settings/Episode - global DOWNLOAD_PROPERS, CHECK_PROPERS_INTERVAL, RECENTSEARCH_FREQUENCY, \ + global DOWNLOAD_PROPERS, PROPERS_WEBDL_ONEGRP, CHECK_PROPERS_INTERVAL, RECENTSEARCH_FREQUENCY, \ BACKLOG_DAYS, BACKLOG_NOFULL, BACKLOG_FREQUENCY, USENET_RETENTION, IGNORE_WORDS, REQUIRE_WORDS, \ ALLOW_HIGH_PRIORITY, SEARCH_UNAIRED, UNAIRED_RECENT_SEARCH_ONLY # Search Settings/NZB search @@ -591,8 +631,7 @@ def initialize(console_logging=True): USE_SYNOINDEX, \ USE_SYNOLOGYNOTIFIER, SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH, \ SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD, SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD, \ - USE_PYTIVO, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \ - PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD, PYTIVO_UPDATE_LIBRARY + USE_PYTIVO, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME # Notification Settings/Devices global USE_GROWL, GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, \ GROWL_HOST, GROWL_PASSWORD, \ @@ -615,6 +654,13 @@ def initialize(console_logging=True): USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_ACCOUNTS, TRAKT_MRU, TRAKT_VERIFY, \ TRAKT_USE_WATCHLIST, TRAKT_REMOVE_WATCHLIST, TRAKT_TIMEOUT, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, \ TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, TRAKT_UPDATE_COLLECTION, \ + USE_SLACK, SLACK_NOTIFY_ONSNATCH, SLACK_NOTIFY_ONDOWNLOAD, SLACK_NOTIFY_ONSUBTITLEDOWNLOAD, \ + SLACK_CHANNEL, SLACK_AS_AUTHED, SLACK_BOT_NAME, SLACK_ICON_URL, SLACK_ACCESS_TOKEN, \ + USE_DISCORDAPP, DISCORDAPP_NOTIFY_ONSNATCH, DISCORDAPP_NOTIFY_ONDOWNLOAD, \ + DISCORDAPP_NOTIFY_ONSUBTITLEDOWNLOAD, \ + DISCORDAPP_AS_AUTHED, DISCORDAPP_USERNAME, DISCORDAPP_ICON_URL, DISCORDAPP_AS_TTS, DISCORDAPP_ACCESS_TOKEN,\ + USE_GITTER, GITTER_NOTIFY_ONSNATCH, GITTER_NOTIFY_ONDOWNLOAD, GITTER_NOTIFY_ONSUBTITLEDOWNLOAD,\ + GITTER_ROOM, GITTER_ACCESS_TOKEN, \ USE_EMAIL, EMAIL_NOTIFY_ONSNATCH, EMAIL_NOTIFY_ONDOWNLOAD, EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD, EMAIL_FROM, \ EMAIL_HOST, EMAIL_PORT, EMAIL_TLS, EMAIL_USER, EMAIL_PASSWORD, EMAIL_LIST, EMAIL_OLD_SUBJECTS # Anime Settings @@ -624,7 +670,8 @@ def initialize(console_logging=True): return False for stanza in ('General', 'Blackhole', 'SABnzbd', 'NZBget', 'Emby', 'Kodi', 'XBMC', 'PLEX', - 'Growl', 'Prowl', 'Twitter', 'Boxcar2', 'NMJ', 'NMJv2', 'Synology', 'SynologyNotifier', + 'Growl', 'Prowl', 'Twitter', 'Slack', 'Discordapp', 'Boxcar2', 'NMJ', 'NMJv2', + 'Synology', 'SynologyNotifier', 'pyTivo', 'NMA', 'Pushalot', 'Pushbullet', 'Subtitles'): CheckSection(CFG, stanza) @@ -703,6 +750,7 @@ def initialize(console_logging=True): WEB_PORT = minimax(check_setting_int(CFG, 'General', 'web_port', 8081), 8081, 21, 65535) WEB_ROOT = check_setting_str(CFG, 'General', 'web_root', '').rstrip('/') WEB_IPV6 = bool(check_setting_int(CFG, 'General', 'web_ipv6', 0)) + WEB_IPV64 = bool(check_setting_int(CFG, 'General', 'web_ipv64', 0)) WEB_LOG = bool(check_setting_int(CFG, 'General', 'web_log', 0)) ENCRYPTION_VERSION = check_setting_int(CFG, 'General', 'encryption_version', 0) WEB_USERNAME = check_setting_str(CFG, 'General', 'web_username', '') @@ -741,6 +789,8 @@ def initialize(console_logging=True): if not re.match(r'\d+\|[^|]+(?:\|[^|]+)*', ROOT_DIRS): ROOT_DIRS = '' + RESULTS_SORTBY = check_setting_str(CFG, 'General', 'results_sortby', '') + QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) WANTED_BEGIN_DEFAULT = check_setting_int(CFG, 'General', 'wanted_begin_default', 0) @@ -786,6 +836,7 @@ def initialize(console_logging=True): TORRENT_METHOD = 'blackhole' DOWNLOAD_PROPERS = bool(check_setting_int(CFG, 'General', 'download_propers', 1)) + PROPERS_WEBDL_ONEGRP = bool(check_setting_int(CFG, 'General', 'propers_webdl_onegrp', 1)) CHECK_PROPERS_INTERVAL = check_setting_str(CFG, 'General', 'check_propers_interval', '') if CHECK_PROPERS_INTERVAL not in ('15m', '45m', '90m', '4h', 'daily'): CHECK_PROPERS_INTERVAL = 'daily' @@ -836,6 +887,7 @@ def initialize(console_logging=True): CREATE_MISSING_SHOW_DIRS = bool(check_setting_int(CFG, 'General', 'create_missing_show_dirs', 0)) ADD_SHOWS_WO_DIR = bool(check_setting_int(CFG, 'General', 'add_shows_wo_dir', 0)) REMOVE_FILENAME_CHARS = check_setting_str(CFG, 'General', 'remove_filename_chars', '') + IMPORT_DEFAULT_CHECKED_SHOWS = bool(check_setting_int(CFG, 'General', 'import_default_checked_shows', 0)) SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '') @@ -982,10 +1034,6 @@ def initialize(console_logging=True): CheckSection(CFG, 'pyTivo') USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0)) - PYTIVO_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_onsnatch', 0)) - PYTIVO_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_ondownload', 0)) - PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_onsubtitledownload', 0)) - PYTIVO_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'pyTivo', 'pyTivo_update_library', 0)) PYTIVO_HOST = check_setting_str(CFG, 'pyTivo', 'pytivo_host', '') PYTIVO_SHARE_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_share_name', '') PYTIVO_TIVO_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_tivo_name', '') @@ -1012,6 +1060,34 @@ def initialize(console_logging=True): PUSHBULLET_ACCESS_TOKEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_access_token', '') PUSHBULLET_DEVICE_IDEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device_iden', '') + USE_SLACK = bool(check_setting_int(CFG, 'Slack', 'use_slack', 0)) + SLACK_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Slack', 'slack_notify_onsnatch', 0)) + SLACK_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Slack', 'slack_notify_ondownload', 0)) + SLACK_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Slack', 'slack_notify_onsubtitledownload', 0)) + SLACK_CHANNEL = check_setting_str(CFG, 'Slack', 'slack_channel', '') + SLACK_AS_AUTHED = bool(check_setting_int(CFG, 'Slack', 'slack_as_authed', 0)) + SLACK_BOT_NAME = check_setting_str(CFG, 'Slack', 'slack_bot_name', '') + SLACK_ICON_URL = check_setting_str(CFG, 'Slack', 'slack_icon_url', '') + SLACK_ACCESS_TOKEN = check_setting_str(CFG, 'Slack', 'slack_access_token', '') + + USE_DISCORDAPP = bool(check_setting_int(CFG, 'Discordapp', 'use_discordapp', 0)) + DISCORDAPP_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Discordapp', 'discordapp_notify_onsnatch', 0)) + DISCORDAPP_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Discordapp', 'discordapp_notify_ondownload', 0)) + DISCORDAPP_NOTIFY_ONSUBTITLEDOWNLOAD = bool( + check_setting_int(CFG, 'Discordapp', 'discordapp_notify_onsubtitledownload', 0)) + DISCORDAPP_AS_AUTHED = bool(check_setting_int(CFG, 'Discordapp', 'discordapp_as_authed', 0)) + DISCORDAPP_USERNAME = check_setting_str(CFG, 'Discordapp', 'discordapp_username', '') + DISCORDAPP_ICON_URL = check_setting_str(CFG, 'Discordapp', 'discordapp_icon_url', '') + DISCORDAPP_AS_TTS = bool(check_setting_str(CFG, 'Discordapp', 'discordapp_as_tts', 0)) + DISCORDAPP_ACCESS_TOKEN = check_setting_str(CFG, 'Discordapp', 'discordapp_access_token', '') + + USE_GITTER = bool(check_setting_int(CFG, 'Gitter', 'use_gitter', 0)) + GITTER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Gitter', 'gitter_notify_onsnatch', 0)) + GITTER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Gitter', 'gitter_notify_ondownload', 0)) + GITTER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Gitter', 'gitter_notify_onsubtitledownload', 0)) + GITTER_ROOM = check_setting_str(CFG, 'Gitter', 'gitter_room', '') + GITTER_ACCESS_TOKEN = check_setting_str(CFG, 'Gitter', 'gitter_access_token', '') + USE_EMAIL = bool(check_setting_int(CFG, 'Email', 'use_email', 0)) EMAIL_OLD_SUBJECTS = bool(check_setting_int(CFG, 'Email', 'email_old_subjects', None is not EMAIL_HOST and any(EMAIL_HOST))) @@ -1090,6 +1166,9 @@ def initialize(console_logging=True): EPISODE_VIEW_MISSED_RANGE = check_setting_int(CFG, 'GUI', 'episode_view_missed_range', 7) HISTORY_LAYOUT = check_setting_str(CFG, 'GUI', 'history_layout', 'detailed') + BROWSELIST_HIDDEN = [ + x.strip() for x in check_setting_str(CFG, 'GUI', 'browselist_hidden', '').split('|~|') if x.strip()] + # initialize NZB and TORRENT providers providerList = providers.makeProviderList() @@ -1143,6 +1222,9 @@ def initialize(console_logging=True): not getattr(torrent_prov, 'supports_backlog') if hasattr(torrent_prov, 'enable_backlog'): torrent_prov.enable_backlog = bool(check_setting_int(CFG, prov_id_uc, prov_id + '_enable_backlog', 1)) + if hasattr(torrent_prov, 'enable_scheduled_backlog'): + torrent_prov.enable_scheduled_backlog = bool(check_setting_int( + CFG, prov_id_uc, prov_id + '_enable_scheduled_backlog', 1)) if hasattr(torrent_prov, 'search_mode'): torrent_prov.search_mode = check_setting_str(CFG, prov_id_uc, prov_id + '_search_mode', 'eponly') if hasattr(torrent_prov, 'search_fallback'): @@ -1170,6 +1252,9 @@ def initialize(console_logging=True): not getattr(nzb_prov, 'supports_backlog') if hasattr(nzb_prov, 'enable_backlog'): nzb_prov.enable_backlog = bool(check_setting_int(CFG, prov_id_uc, prov_id + '_enable_backlog', 1)) + if hasattr(nzb_prov, 'enable_scheduled_backlog'): + nzb_prov.enable_scheduled_backlog = bool(check_setting_int( + CFG, prov_id_uc, prov_id + '_enable_scheduled_backlog', 1)) if not os.path.isfile(CONFIG_FILE): logger.log(u'Unable to find \'%s\', all settings will be default!' % CONFIG_FILE, logger.DEBUG) @@ -1344,6 +1429,13 @@ def start(): indexermapper.indexer_list = [i for i in indexerApi().all_indexers] background_mapping_task.start() + for p in providers.sortedProviderList(): + if p.is_active() and getattr(p, 'ping_freq', None): + # noinspection PyProtectedMember + provider_ping_thread_pool[p.get_id()] = threading.Thread( + name='PING-PROVIDER %s' % p.name, target=p._ping) + provider_ping_thread_pool[p.get_id()].start() + for thread in enabled_schedulers(is_init=True): thread.start() @@ -1378,30 +1470,41 @@ def halt(): if __INITIALIZED__: - logger.log(u'Aborting all threads') + logger.log('Exiting threads') - for thread in enabled_schedulers(): - thread.stop.set() + for p in provider_ping_thread_pool: + provider_ping_thread_pool[p].stop = True - for thread in enabled_schedulers(): - logger.log('Waiting for the %s thread to exit' % thread.name) + for p in provider_ping_thread_pool: try: - thread.join(10) + provider_ping_thread_pool[p].join(10) + logger.log('Thread %s has exit' % provider_ping_thread_pool[p].name) except RuntimeError: + logger.log('Fail, thread %s did not exit' % provider_ping_thread_pool[p].name) pass if ADBA_CONNECTION: try: ADBA_CONNECTION.logout() except AniDBBannedError as e: - logger.log(u'ANIDB Error %s' % ex(e), logger.DEBUG) + logger.log('AniDB Error %s' % ex(e), logger.DEBUG) except AniDBError: pass - logger.log(u'Waiting for the ANIDB CONNECTION thread to exit') try: ADBA_CONNECTION.join(10) + logger.log('Thread %s has exit' % ADBA_CONNECTION.name) except (StandardError, Exception): - pass + logger.log('Fail, thread %s did not exit' % ADBA_CONNECTION.name) + + for thread in enabled_schedulers(): + thread.stop.set() + + for thread in enabled_schedulers(): + try: + thread.join(10) + logger.log('Thread %s has exit' % thread.name) + except RuntimeError: + logger.log('Thread %s did not exit' % thread.name) __INITIALIZED__ = False started = False @@ -1427,6 +1530,9 @@ def save_config(): # For passwords you must include the word `password` in the item_name and # add `helpers.encrypt(ITEM_NAME, ENCRYPTION_VERSION)` in save_config() new_config['General'] = {} + s_z = check_setting_int(CFG, 'General', 'stack_size', 0) + if s_z: + new_config['General']['stack_size'] = s_z new_config['General']['config_version'] = CONFIG_VERSION new_config['General']['branch'] = BRANCH new_config['General']['git_remote'] = GIT_REMOTE @@ -1439,6 +1545,7 @@ def save_config(): new_config['General']['web_host'] = WEB_HOST new_config['General']['web_port'] = WEB_PORT new_config['General']['web_ipv6'] = int(WEB_IPV6) + new_config['General']['web_ipv64'] = int(WEB_IPV64) new_config['General']['web_log'] = int(WEB_LOG) new_config['General']['web_root'] = WEB_ROOT new_config['General']['web_username'] = WEB_USERNAME @@ -1462,11 +1569,13 @@ def save_config(): new_config['General']['backlog_frequency'] = int(BACKLOG_FREQUENCY) new_config['General']['update_frequency'] = int(UPDATE_FREQUENCY) new_config['General']['download_propers'] = int(DOWNLOAD_PROPERS) + new_config['General']['propers_webdl_onegrp'] = int(PROPERS_WEBDL_ONEGRP) new_config['General']['check_propers_interval'] = CHECK_PROPERS_INTERVAL new_config['General']['allow_high_priority'] = int(ALLOW_HIGH_PRIORITY) new_config['General']['recentsearch_startup'] = int(RECENTSEARCH_STARTUP) new_config['General']['backlog_nofull'] = int(BACKLOG_NOFULL) new_config['General']['skip_removed_files'] = int(SKIP_REMOVED_FILES) + new_config['General']['results_sortby'] = str(RESULTS_SORTBY) new_config['General']['quality_default'] = int(QUALITY_DEFAULT) new_config['General']['status_default'] = int(STATUS_DEFAULT) new_config['General']['wanted_begin_default'] = int(WANTED_BEGIN_DEFAULT) @@ -1531,6 +1640,7 @@ def save_config(): new_config['General']['create_missing_show_dirs'] = int(CREATE_MISSING_SHOW_DIRS) new_config['General']['add_shows_wo_dir'] = int(ADD_SHOWS_WO_DIR) new_config['General']['remove_filename_chars'] = REMOVE_FILENAME_CHARS + new_config['General']['import_default_checked_shows'] = int(IMPORT_DEFAULT_CHECKED_SHOWS) new_config['General']['extra_scripts'] = '|'.join(EXTRA_SCRIPTS) new_config['General']['git_path'] = GIT_PATH @@ -1559,7 +1669,7 @@ def save_config(): ('api_key', None), ('passkey', None), ('digest', None), ('hash', None), ('username', ''), ('uid', ''), ('minseed', 1), ('minleech', 1), ('confirmed', 1), ('freeleech', 1), ('reject_m2ts', 1), ('enable_recentsearch', 1), ('enable_backlog', 1), ('search_mode', None), ('search_fallback', 1), - ('seed_time', None)] if hasattr(src, k)]: + ('seed_time', None), ('enable_scheduled_backlog', 1)] if hasattr(src, k)]: new_config[src_id_uc][setting] = value if hasattr(src, '_seed_ratio'): @@ -1576,7 +1686,8 @@ def save_config(): for attr in [x for x in ['api_key', 'username', 'search_mode'] if hasattr(src, x)]: new_config[src_id_uc]['%s_%s' % (src_id, attr)] = getattr(src, attr) - for attr in [x for x in ['enable_recentsearch', 'enable_backlog', 'search_fallback'] if hasattr(src, x)]: + for attr in [x for x in ['enable_recentsearch', 'enable_backlog', 'search_fallback', + 'enable_scheduled_backlog'] if hasattr(src, x)]: new_config[src_id_uc]['%s_%s' % (src_id, attr)] = helpers.tryInt(getattr(src, attr, None)) new_config['SABnzbd'] = {} @@ -1615,90 +1726,40 @@ def save_config(): new_config['Kodi'] = {} new_config['Kodi']['use_kodi'] = int(USE_KODI) new_config['Kodi']['kodi_always_on'] = int(KODI_ALWAYS_ON) - new_config['Kodi']['kodi_notify_onsnatch'] = int(KODI_NOTIFY_ONSNATCH) - new_config['Kodi']['kodi_notify_ondownload'] = int(KODI_NOTIFY_ONDOWNLOAD) - new_config['Kodi']['kodi_notify_onsubtitledownload'] = int(KODI_NOTIFY_ONSUBTITLEDOWNLOAD) new_config['Kodi']['kodi_update_library'] = int(KODI_UPDATE_LIBRARY) new_config['Kodi']['kodi_update_full'] = int(KODI_UPDATE_FULL) new_config['Kodi']['kodi_update_onlyfirst'] = int(KODI_UPDATE_ONLYFIRST) new_config['Kodi']['kodi_host'] = KODI_HOST new_config['Kodi']['kodi_username'] = KODI_USERNAME new_config['Kodi']['kodi_password'] = helpers.encrypt(KODI_PASSWORD, ENCRYPTION_VERSION) + new_config['Kodi']['kodi_notify_onsnatch'] = int(KODI_NOTIFY_ONSNATCH) + new_config['Kodi']['kodi_notify_ondownload'] = int(KODI_NOTIFY_ONDOWNLOAD) + new_config['Kodi']['kodi_notify_onsubtitledownload'] = int(KODI_NOTIFY_ONSUBTITLEDOWNLOAD) + + new_config['Plex'] = {} + new_config['Plex']['use_plex'] = int(USE_PLEX) + new_config['Plex']['plex_username'] = PLEX_USERNAME + new_config['Plex']['plex_password'] = helpers.encrypt(PLEX_PASSWORD, ENCRYPTION_VERSION) + new_config['Plex']['plex_update_library'] = int(PLEX_UPDATE_LIBRARY) + new_config['Plex']['plex_server_host'] = PLEX_SERVER_HOST + new_config['Plex']['plex_notify_onsnatch'] = int(PLEX_NOTIFY_ONSNATCH) + new_config['Plex']['plex_notify_ondownload'] = int(PLEX_NOTIFY_ONDOWNLOAD) + new_config['Plex']['plex_notify_onsubtitledownload'] = int(PLEX_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Plex']['plex_host'] = PLEX_HOST new_config['XBMC'] = {} new_config['XBMC']['use_xbmc'] = int(USE_XBMC) new_config['XBMC']['xbmc_always_on'] = int(XBMC_ALWAYS_ON) - new_config['XBMC']['xbmc_notify_onsnatch'] = int(XBMC_NOTIFY_ONSNATCH) - new_config['XBMC']['xbmc_notify_ondownload'] = int(XBMC_NOTIFY_ONDOWNLOAD) - new_config['XBMC']['xbmc_notify_onsubtitledownload'] = int(XBMC_NOTIFY_ONSUBTITLEDOWNLOAD) new_config['XBMC']['xbmc_update_library'] = int(XBMC_UPDATE_LIBRARY) new_config['XBMC']['xbmc_update_full'] = int(XBMC_UPDATE_FULL) new_config['XBMC']['xbmc_update_onlyfirst'] = int(XBMC_UPDATE_ONLYFIRST) + new_config['XBMC']['xbmc_notify_onsnatch'] = int(XBMC_NOTIFY_ONSNATCH) + new_config['XBMC']['xbmc_notify_ondownload'] = int(XBMC_NOTIFY_ONDOWNLOAD) + new_config['XBMC']['xbmc_notify_onsubtitledownload'] = int(XBMC_NOTIFY_ONSUBTITLEDOWNLOAD) new_config['XBMC']['xbmc_host'] = XBMC_HOST new_config['XBMC']['xbmc_username'] = XBMC_USERNAME new_config['XBMC']['xbmc_password'] = helpers.encrypt(XBMC_PASSWORD, ENCRYPTION_VERSION) - new_config['Plex'] = {} - new_config['Plex']['use_plex'] = int(USE_PLEX) - new_config['Plex']['plex_notify_onsnatch'] = int(PLEX_NOTIFY_ONSNATCH) - new_config['Plex']['plex_notify_ondownload'] = int(PLEX_NOTIFY_ONDOWNLOAD) - new_config['Plex']['plex_notify_onsubtitledownload'] = int(PLEX_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Plex']['plex_update_library'] = int(PLEX_UPDATE_LIBRARY) - new_config['Plex']['plex_server_host'] = PLEX_SERVER_HOST - new_config['Plex']['plex_host'] = PLEX_HOST - new_config['Plex']['plex_username'] = PLEX_USERNAME - new_config['Plex']['plex_password'] = helpers.encrypt(PLEX_PASSWORD, ENCRYPTION_VERSION) - - new_config['Growl'] = {} - new_config['Growl']['use_growl'] = int(USE_GROWL) - new_config['Growl']['growl_notify_onsnatch'] = int(GROWL_NOTIFY_ONSNATCH) - new_config['Growl']['growl_notify_ondownload'] = int(GROWL_NOTIFY_ONDOWNLOAD) - new_config['Growl']['growl_notify_onsubtitledownload'] = int(GROWL_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Growl']['growl_host'] = GROWL_HOST - new_config['Growl']['growl_password'] = helpers.encrypt(GROWL_PASSWORD, ENCRYPTION_VERSION) - - new_config['Prowl'] = {} - new_config['Prowl']['use_prowl'] = int(USE_PROWL) - new_config['Prowl']['prowl_notify_onsnatch'] = int(PROWL_NOTIFY_ONSNATCH) - new_config['Prowl']['prowl_notify_ondownload'] = int(PROWL_NOTIFY_ONDOWNLOAD) - new_config['Prowl']['prowl_notify_onsubtitledownload'] = int(PROWL_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Prowl']['prowl_api'] = PROWL_API - new_config['Prowl']['prowl_priority'] = PROWL_PRIORITY - - new_config['Twitter'] = {} - new_config['Twitter']['use_twitter'] = int(USE_TWITTER) - new_config['Twitter']['twitter_notify_onsnatch'] = int(TWITTER_NOTIFY_ONSNATCH) - new_config['Twitter']['twitter_notify_ondownload'] = int(TWITTER_NOTIFY_ONDOWNLOAD) - new_config['Twitter']['twitter_notify_onsubtitledownload'] = int(TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Twitter']['twitter_username'] = TWITTER_USERNAME - new_config['Twitter']['twitter_password'] = helpers.encrypt(TWITTER_PASSWORD, ENCRYPTION_VERSION) - new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX - - new_config['Boxcar2'] = {} - new_config['Boxcar2']['use_boxcar2'] = int(USE_BOXCAR2) - new_config['Boxcar2']['boxcar2_notify_onsnatch'] = int(BOXCAR2_NOTIFY_ONSNATCH) - new_config['Boxcar2']['boxcar2_notify_ondownload'] = int(BOXCAR2_NOTIFY_ONDOWNLOAD) - new_config['Boxcar2']['boxcar2_notify_onsubtitledownload'] = int(BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Boxcar2']['boxcar2_accesstoken'] = BOXCAR2_ACCESSTOKEN - new_config['Boxcar2']['boxcar2_sound'] = BOXCAR2_SOUND - - new_config['Pushover'] = {} - new_config['Pushover']['use_pushover'] = int(USE_PUSHOVER) - new_config['Pushover']['pushover_notify_onsnatch'] = int(PUSHOVER_NOTIFY_ONSNATCH) - new_config['Pushover']['pushover_notify_ondownload'] = int(PUSHOVER_NOTIFY_ONDOWNLOAD) - new_config['Pushover']['pushover_notify_onsubtitledownload'] = int(PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Pushover']['pushover_userkey'] = PUSHOVER_USERKEY - new_config['Pushover']['pushover_apikey'] = PUSHOVER_APIKEY - new_config['Pushover']['pushover_priority'] = int(PUSHOVER_PRIORITY) - new_config['Pushover']['pushover_device'] = PUSHOVER_DEVICE - new_config['Pushover']['pushover_sound'] = PUSHOVER_SOUND - - new_config['Libnotify'] = {} - new_config['Libnotify']['use_libnotify'] = int(USE_LIBNOTIFY) - new_config['Libnotify']['libnotify_notify_onsnatch'] = int(LIBNOTIFY_NOTIFY_ONSNATCH) - new_config['Libnotify']['libnotify_notify_ondownload'] = int(LIBNOTIFY_NOTIFY_ONDOWNLOAD) - new_config['Libnotify']['libnotify_notify_onsubtitledownload'] = int(LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['NMJ'] = {} new_config['NMJ']['use_nmj'] = int(USE_NMJ) new_config['NMJ']['nmj_host'] = NMJ_HOST @@ -1721,6 +1782,76 @@ def save_config(): new_config['SynologyNotifier']['synologynotifier_notify_onsubtitledownload'] = int( SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['pyTivo'] = {} + new_config['pyTivo']['use_pytivo'] = int(USE_PYTIVO) + new_config['pyTivo']['pytivo_host'] = PYTIVO_HOST + new_config['pyTivo']['pytivo_share_name'] = PYTIVO_SHARE_NAME + new_config['pyTivo']['pytivo_tivo_name'] = PYTIVO_TIVO_NAME + + new_config['Boxcar2'] = {} + new_config['Boxcar2']['use_boxcar2'] = int(USE_BOXCAR2) + new_config['Boxcar2']['boxcar2_notify_onsnatch'] = int(BOXCAR2_NOTIFY_ONSNATCH) + new_config['Boxcar2']['boxcar2_notify_ondownload'] = int(BOXCAR2_NOTIFY_ONDOWNLOAD) + new_config['Boxcar2']['boxcar2_notify_onsubtitledownload'] = int(BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Boxcar2']['boxcar2_accesstoken'] = BOXCAR2_ACCESSTOKEN + new_config['Boxcar2']['boxcar2_sound'] = BOXCAR2_SOUND + + new_config['Pushbullet'] = {} + new_config['Pushbullet']['use_pushbullet'] = int(USE_PUSHBULLET) + new_config['Pushbullet']['pushbullet_notify_onsnatch'] = int(PUSHBULLET_NOTIFY_ONSNATCH) + new_config['Pushbullet']['pushbullet_notify_ondownload'] = int(PUSHBULLET_NOTIFY_ONDOWNLOAD) + new_config['Pushbullet']['pushbullet_notify_onsubtitledownload'] = int(PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Pushbullet']['pushbullet_access_token'] = PUSHBULLET_ACCESS_TOKEN + new_config['Pushbullet']['pushbullet_device_iden'] = PUSHBULLET_DEVICE_IDEN + + new_config['Pushover'] = {} + new_config['Pushover']['use_pushover'] = int(USE_PUSHOVER) + new_config['Pushover']['pushover_notify_onsnatch'] = int(PUSHOVER_NOTIFY_ONSNATCH) + new_config['Pushover']['pushover_notify_ondownload'] = int(PUSHOVER_NOTIFY_ONDOWNLOAD) + new_config['Pushover']['pushover_notify_onsubtitledownload'] = int(PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Pushover']['pushover_userkey'] = PUSHOVER_USERKEY + new_config['Pushover']['pushover_apikey'] = PUSHOVER_APIKEY + new_config['Pushover']['pushover_priority'] = int(PUSHOVER_PRIORITY) + new_config['Pushover']['pushover_device'] = PUSHOVER_DEVICE + new_config['Pushover']['pushover_sound'] = PUSHOVER_SOUND + + new_config['Growl'] = {} + new_config['Growl']['use_growl'] = int(USE_GROWL) + new_config['Growl']['growl_notify_onsnatch'] = int(GROWL_NOTIFY_ONSNATCH) + new_config['Growl']['growl_notify_ondownload'] = int(GROWL_NOTIFY_ONDOWNLOAD) + new_config['Growl']['growl_notify_onsubtitledownload'] = int(GROWL_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Growl']['growl_host'] = GROWL_HOST + new_config['Growl']['growl_password'] = helpers.encrypt(GROWL_PASSWORD, ENCRYPTION_VERSION) + + new_config['Prowl'] = {} + new_config['Prowl']['use_prowl'] = int(USE_PROWL) + new_config['Prowl']['prowl_notify_onsnatch'] = int(PROWL_NOTIFY_ONSNATCH) + new_config['Prowl']['prowl_notify_ondownload'] = int(PROWL_NOTIFY_ONDOWNLOAD) + new_config['Prowl']['prowl_notify_onsubtitledownload'] = int(PROWL_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Prowl']['prowl_api'] = PROWL_API + new_config['Prowl']['prowl_priority'] = PROWL_PRIORITY + + new_config['NMA'] = {} + new_config['NMA']['use_nma'] = int(USE_NMA) + new_config['NMA']['nma_notify_onsnatch'] = int(NMA_NOTIFY_ONSNATCH) + new_config['NMA']['nma_notify_ondownload'] = int(NMA_NOTIFY_ONDOWNLOAD) + new_config['NMA']['nma_notify_onsubtitledownload'] = int(NMA_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['NMA']['nma_api'] = NMA_API + new_config['NMA']['nma_priority'] = NMA_PRIORITY + + new_config['Libnotify'] = {} + new_config['Libnotify']['use_libnotify'] = int(USE_LIBNOTIFY) + new_config['Libnotify']['libnotify_notify_onsnatch'] = int(LIBNOTIFY_NOTIFY_ONSNATCH) + new_config['Libnotify']['libnotify_notify_ondownload'] = int(LIBNOTIFY_NOTIFY_ONDOWNLOAD) + new_config['Libnotify']['libnotify_notify_onsubtitledownload'] = int(LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD) + + new_config['Pushalot'] = {} + new_config['Pushalot']['use_pushalot'] = int(USE_PUSHALOT) + new_config['Pushalot']['pushalot_notify_onsnatch'] = int(PUSHALOT_NOTIFY_ONSNATCH) + new_config['Pushalot']['pushalot_notify_ondownload'] = int(PUSHALOT_NOTIFY_ONDOWNLOAD) + new_config['Pushalot']['pushalot_notify_onsubtitledownload'] = int(PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Pushalot']['pushalot_authorizationtoken'] = PUSHALOT_AUTHORIZATIONTOKEN + new_config['Trakt'] = {} new_config['Trakt']['use_trakt'] = int(USE_TRAKT) new_config['Trakt']['trakt_remove_watchlist'] = int(TRAKT_REMOVE_WATCHLIST) @@ -1734,38 +1865,44 @@ def save_config(): new_config['Trakt']['trakt_accounts'] = TraktAPI.build_config_string(TRAKT_ACCOUNTS) new_config['Trakt']['trakt_mru'] = TRAKT_MRU - new_config['pyTivo'] = {} - new_config['pyTivo']['use_pytivo'] = int(USE_PYTIVO) - new_config['pyTivo']['pytivo_notify_onsnatch'] = int(PYTIVO_NOTIFY_ONSNATCH) - new_config['pyTivo']['pytivo_notify_ondownload'] = int(PYTIVO_NOTIFY_ONDOWNLOAD) - new_config['pyTivo']['pytivo_notify_onsubtitledownload'] = int(PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['pyTivo']['pyTivo_update_library'] = int(PYTIVO_UPDATE_LIBRARY) - new_config['pyTivo']['pytivo_host'] = PYTIVO_HOST - new_config['pyTivo']['pytivo_share_name'] = PYTIVO_SHARE_NAME - new_config['pyTivo']['pytivo_tivo_name'] = PYTIVO_TIVO_NAME + new_config['Slack'] = {} + new_config['Slack']['use_slack'] = int(USE_SLACK) + new_config['Slack']['slack_notify_onsnatch'] = int(SLACK_NOTIFY_ONSNATCH) + new_config['Slack']['slack_notify_ondownload'] = int(SLACK_NOTIFY_ONDOWNLOAD) + new_config['Slack']['slack_notify_onsubtitledownload'] = int(SLACK_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Slack']['slack_channel'] = SLACK_CHANNEL + new_config['Slack']['slack_as_authed'] = int(SLACK_AS_AUTHED) + new_config['Slack']['slack_bot_name'] = SLACK_BOT_NAME + new_config['Slack']['slack_icon_url'] = SLACK_ICON_URL + new_config['Slack']['slack_access_token'] = SLACK_ACCESS_TOKEN - new_config['NMA'] = {} - new_config['NMA']['use_nma'] = int(USE_NMA) - new_config['NMA']['nma_notify_onsnatch'] = int(NMA_NOTIFY_ONSNATCH) - new_config['NMA']['nma_notify_ondownload'] = int(NMA_NOTIFY_ONDOWNLOAD) - new_config['NMA']['nma_notify_onsubtitledownload'] = int(NMA_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['NMA']['nma_api'] = NMA_API - new_config['NMA']['nma_priority'] = NMA_PRIORITY + new_config['Discordapp'] = {} + new_config['Discordapp']['use_discordapp'] = int(USE_DISCORDAPP) + new_config['Discordapp']['discordapp_notify_onsnatch'] = int(DISCORDAPP_NOTIFY_ONSNATCH) + new_config['Discordapp']['discordapp_notify_ondownload'] = int(DISCORDAPP_NOTIFY_ONDOWNLOAD) + new_config['Discordapp']['discordapp_notify_onsubtitledownload'] = int(DISCORDAPP_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Discordapp']['discordapp_as_authed'] = int(DISCORDAPP_AS_AUTHED) + new_config['Discordapp']['discordapp_username'] = DISCORDAPP_USERNAME + new_config['Discordapp']['discordapp_icon_url'] = DISCORDAPP_ICON_URL + new_config['Discordapp']['discordapp_as_tts'] = int(DISCORDAPP_AS_TTS) + new_config['Discordapp']['discordapp_access_token'] = DISCORDAPP_ACCESS_TOKEN - new_config['Pushalot'] = {} - new_config['Pushalot']['use_pushalot'] = int(USE_PUSHALOT) - new_config['Pushalot']['pushalot_notify_onsnatch'] = int(PUSHALOT_NOTIFY_ONSNATCH) - new_config['Pushalot']['pushalot_notify_ondownload'] = int(PUSHALOT_NOTIFY_ONDOWNLOAD) - new_config['Pushalot']['pushalot_notify_onsubtitledownload'] = int(PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Pushalot']['pushalot_authorizationtoken'] = PUSHALOT_AUTHORIZATIONTOKEN + new_config['Gitter'] = {} + new_config['Gitter']['use_gitter'] = int(USE_GITTER) + new_config['Gitter']['gitter_notify_onsnatch'] = int(GITTER_NOTIFY_ONSNATCH) + new_config['Gitter']['gitter_notify_ondownload'] = int(GITTER_NOTIFY_ONDOWNLOAD) + new_config['Gitter']['gitter_notify_onsubtitledownload'] = int(GITTER_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Gitter']['gitter_room'] = GITTER_ROOM + new_config['Gitter']['gitter_access_token'] = GITTER_ACCESS_TOKEN - new_config['Pushbullet'] = {} - new_config['Pushbullet']['use_pushbullet'] = int(USE_PUSHBULLET) - new_config['Pushbullet']['pushbullet_notify_onsnatch'] = int(PUSHBULLET_NOTIFY_ONSNATCH) - new_config['Pushbullet']['pushbullet_notify_ondownload'] = int(PUSHBULLET_NOTIFY_ONDOWNLOAD) - new_config['Pushbullet']['pushbullet_notify_onsubtitledownload'] = int(PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Pushbullet']['pushbullet_access_token'] = PUSHBULLET_ACCESS_TOKEN - new_config['Pushbullet']['pushbullet_device_iden'] = PUSHBULLET_DEVICE_IDEN + new_config['Twitter'] = {} + new_config['Twitter']['use_twitter'] = int(USE_TWITTER) + new_config['Twitter']['twitter_notify_onsnatch'] = int(TWITTER_NOTIFY_ONSNATCH) + new_config['Twitter']['twitter_notify_ondownload'] = int(TWITTER_NOTIFY_ONDOWNLOAD) + new_config['Twitter']['twitter_notify_onsubtitledownload'] = int(TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Twitter']['twitter_username'] = TWITTER_USERNAME + new_config['Twitter']['twitter_password'] = helpers.encrypt(TWITTER_PASSWORD, ENCRYPTION_VERSION) + new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX new_config['Email'] = {} new_config['Email']['use_email'] = int(USE_EMAIL) @@ -1830,6 +1967,7 @@ def save_config(): new_config['GUI']['showlist_tagview'] = SHOWLIST_TAGVIEW new_config['GUI']['show_tag_default'] = SHOW_TAG_DEFAULT new_config['GUI']['history_layout'] = HISTORY_LAYOUT + new_config['GUI']['browselist_hidden'] = '|~|'.join(BROWSELIST_HIDDEN) new_config['Subtitles'] = {} new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES) diff --git a/sickbeard/auto_post_processer.py b/sickbeard/auto_post_processer.py index e888fb45..e8bc66b9 100644 --- a/sickbeard/auto_post_processer.py +++ b/sickbeard/auto_post_processer.py @@ -43,6 +43,6 @@ class PostProcesser(): '(and probably not what you really want to process)' % sickbeard.TV_DOWNLOAD_DIR, logger.ERROR) return - processTV.processDir(sickbeard.TV_DOWNLOAD_DIR) + processTV.processDir(sickbeard.TV_DOWNLOAD_DIR, is_basedir=True) self.amActive = False diff --git a/sickbeard/classes.py b/sickbeard/classes.py index 3338a15c..0102b76e 100644 --- a/sickbeard/classes.py +++ b/sickbeard/classes.py @@ -19,8 +19,8 @@ import re import datetime import sickbeard -from lib.dateutil import parser from sickbeard.common import Quality +from unidecode import unidecode try: from collections import OrderedDict @@ -28,7 +28,7 @@ except ImportError: from requests.compat import OrderedDict -class SearchResult: +class SearchResult(object): """ Represents a search result from an indexer. """ @@ -45,6 +45,9 @@ class SearchResult: # used by some providers to store extra info associated with the result self.extraInfo = [] + # assign function to get the data for the download + self.get_data_func = None + # list of TVEpisode objects that this result is associated with self.episodes = episodes @@ -63,25 +66,48 @@ class SearchResult: # version self.version = -1 + # proper level + self._properlevel = 0 + + # is a repack + self.is_repack = False + + # provider unique id + self.puid = None + + @property + def properlevel(self): + return self._properlevel + + @properlevel.setter + def properlevel(self, v): + if isinstance(v, (int, long)): + self._properlevel = v + def __str__(self): if self.provider is None: return 'Invalid provider, unable to print self' - myString = '%s @ %s\n' % (self.provider.name, self.url) - myString += 'Extra Info:\n' - for extra in self.extraInfo: - myString += ' %s\n' % extra - myString += 'Episode: %s\n' % self.episodes - myString += 'Quality: %s\n' % Quality.qualityStrings[self.quality] - myString += 'Name: %s\n' % self.name - myString += 'Size: %s\n' % str(self.size) - myString += 'Release Group: %s\n' % self.release_group + return '\n'.join([ + '%s @ %s' % (self.provider.name, self.url), + 'Extra Info:', + '\n'.join([' %s' % x for x in self.extraInfo]), + 'Episode: %s' % self.episodes, + 'Quality: %s' % Quality.qualityStrings[self.quality], + 'Name: %s' % self.name, + 'Size: %s' % self.size, + 'Release Group: %s' % self.release_group]) - return myString - - def fileName(self): - return self.episodes[0].prettyName() + '.' + self.resultType + def get_data(self): + if None is not self.get_data_func: + try: + return self.get_data_func(self.url) + except (StandardError, Exception): + pass + if self.extraInfo and 0 < len(self.extraInfo): + return self.extraInfo[0] + return None class NZBSearchResult(SearchResult): @@ -109,7 +135,66 @@ class TorrentSearchResult(SearchResult): hash = None -class AllShowsListUI: +class ShowFilter(object): + def __init__(self, config, log=None): + self.config = config + self.log = log + self.bad_names = [re.compile('(?i)%s' % r) for r in ( + '[*]+\s*(?:403:|do not add|dupli[^s]+\s*(?:\d+| age_limit] + self[:] = [x for x in self if self._is_cache_item(x) and age_limit < x[1]] def __repr__(self): - return str([x[0] for x in self if isinstance(x, (tuple, list)) and len(x) == 2]) + return str([x[0] for x in self if self._is_cache_item(x)]) - def __contains__(self, y): + def __contains__(self, url): for x in self: - if isinstance(x, (tuple, list)) and len(x) == 2 and y == x[0]: + if self._is_cache_item(x) and url == x[0]: return True return False - def remove(self, x): - for v in self: - if isinstance(v, (tuple, list)) and len(v) == 2 and v[0] == x: - super(ImageUrlList, self).remove(v) + def remove(self, url): + for x in self: + if self._is_cache_item(x) and url == x[0]: + super(ImageUrlList, self).remove(x) break diff --git a/sickbeard/clients/deluge.py b/sickbeard/clients/deluge.py index 1db6f868..ae8be94f 100644 --- a/sickbeard/clients/deluge.py +++ b/sickbeard/clients/deluge.py @@ -1,8 +1,9 @@ -# Author: Mr_Orange -# URL: http://code.google.com/p/sickbeard/ +# coding=utf-8 # # This file is part of SickGear. # +# Original author: Mr_Orange +# # SickGear is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -30,65 +31,37 @@ class DelugeAPI(GenericClient): super(DelugeAPI, self).__init__('Deluge', host, username, password) - self.url = self.host + 'json' + self.url = '%s/json' % self.host.rstrip('/') + + def _post_json(self, data, process=True): + result = self.session.post(self.url, json=data, timeout=10, verify=sickbeard.TORRENT_VERIFY_CERT) + if process: + return result.json()['result'] + + def _request_json(self, data, process=None): + result = self._request(method='post', json=data, timeout=10) + if process: + return result.json()['result'] def _get_auth(self): - post_data = json.dumps({'method': 'auth.login', - 'params': [self.password], - 'id': 1}) - try: - self.auth = self.session.post( - self.url, - data=post_data.encode('utf-8'), - verify=sickbeard.TORRENT_VERIFY_CERT - ).json()['result'] + self.auth = self._post_json({'method': 'auth.login', 'params': [self.password], 'id': 1}) - post_data = json.dumps({'method': 'web.connected', - 'params': [], - 'id': 10}) - - connected = self.session.post( - self.url, - data=post_data.encode('utf-8'), - verify=sickbeard.TORRENT_VERIFY_CERT - ).json()['result'] + connected = self._post_json({'method': 'web.connected', 'params': [], 'id': 10}) if not connected: - post_data = json.dumps({'method': 'web.get_hosts', - 'params': [], - 'id': 11}) - - hosts = self.session.post( - self.url, - data=post_data.encode('utf-8'), - verify=sickbeard.TORRENT_VERIFY_CERT - ).json()['result'] - - if len(hosts) == 0: - logger.log(self.name + u': WebUI does not contain daemons', - logger.ERROR) + hosts = self._post_json({'method': 'web.get_hosts', 'params': [], 'id': 11}) + if 0 == len(hosts): + logger.log('%s: WebUI does not contain daemons' % self.name, logger.ERROR) return None - post_data = json.dumps({'method': 'web.connect', - 'params': [hosts[0][0]], - 'id': 11}) - self.session.post(self.url, data=post_data.encode('utf-8'), - verify=sickbeard.TORRENT_VERIFY_CERT) + self._post_json({'method': 'web.connect', 'params': [hosts[0][0]], 'id': 11}, False) - post_data = json.dumps({'method': 'web.connected', - 'params': [], - 'id': 10}) - connected = self.session.post( - self.url, - data=post_data.encode('utf-8'), - verify=sickbeard.TORRENT_VERIFY_CERT - ).json()['result'] + connected = self._post_json({'method': 'web.connected', 'params': [], 'id': 10}) if not connected: - logger.log(self.name + u': WebUI could not connect to daemon', - logger.ERROR) + logger.log('%s: WebUI could not connect to daemon' % self.name, logger.ERROR) return None except RequestException: return None @@ -97,31 +70,24 @@ class DelugeAPI(GenericClient): def _add_torrent_uri(self, result): - post_data = json.dumps({ + result.hash = self._request_json({ 'method': 'core.add_torrent_magnet', - 'params': [result.url, { - 'move_completed': 'true', - 'move_completed_path': sickbeard.TV_DOWNLOAD_DIR - }], - 'id': 2 - }) - result.hash = self._request(method='post', - data=post_data).json()['result'] + 'params': [result.url, + {'move_completed': 'true', + 'move_completed_path': sickbeard.TV_DOWNLOAD_DIR}], + 'id': 2}, True) return result.hash def _add_torrent_file(self, result): - post_data = json.dumps({'method': - 'core.add_torrent_file', - 'params': [result.name + '.torrent', - b64encode(result.content), - {'move_completed': 'true', - 'move_completed_path': - sickbeard.TV_DOWNLOAD_DIR}], - 'id': 2}) - result.hash = self._request(method='post', - data=post_data).json()['result'] + result.hash = self._request_json({ + 'method': 'core.add_torrent_file', + 'params': ['%s.torrent' % result.name, + b64encode(result.content), + {'move_completed': 'true', + 'move_completed_path': sickbeard.TV_DOWNLOAD_DIR}], + 'id': 2}, True) return result.hash @@ -129,50 +95,34 @@ class DelugeAPI(GenericClient): label = sickbeard.TORRENT_LABEL if ' ' in label: - logger.log(self.name + - u': Invalid label. Label must not contain a space', - logger.ERROR) + logger.log('%s: Invalid label. Label must not contain a space' % self.name, logger.ERROR) return False if label: # check if label already exists and create it if not - post_data = json.dumps({ + labels = self._request_json({ 'method': 'label.get_labels', 'params': [], - 'id': 3 - }) - labels = self._request(method='post', - data=post_data).json()['result'] + 'id': 3}, True) - if labels is not None: + if None is not labels: if label not in labels: - logger.log(self.name + ': ' + label + - u' label does not exist in ' + - u'Deluge we must add it', + logger.log('%s: %s label does not exist in Deluge we must add it' % (self.name, label), logger.DEBUG) - post_data = json.dumps({ + self._request_json({ 'method': 'label.add', 'params': [label], - 'id': 4 - }) - self._request(method='post', data=post_data) - logger.log(self.name + ': ' + label + - u' label added to Deluge', logger.DEBUG) + 'id': 4}) + logger.log('%s: %s label added to Deluge' % (self.name, label), logger.DEBUG) # add label to torrent - post_data = json.dumps({ + self._request_json({ 'method': 'label.set_torrent', 'params': [result.hash, label], - 'id': 5 - }) - self._request(method='post', data=post_data) - logger.log(self.name + ': ' + label + - u' label added to torrent', - logger.DEBUG) + 'id': 5}) + logger.log('%s: %s label added to torrent' % (self.name, label), logger.DEBUG) else: - logger.log(self.name + ': ' + - u'label plugin not detected', - logger.DEBUG) + logger.log('%s: label plugin not detected' % self.name, logger.DEBUG) return False return True @@ -184,42 +134,38 @@ class DelugeAPI(GenericClient): ratio = result.ratio if ratio: - post_data = json.dumps({'method': 'core.set_torrent_stop_at_ratio', - 'params': [result.hash, True], - 'id': 5}) - self._request(method='post', data=post_data) + self._request_json({ + 'method': 'core.set_torrent_stop_at_ratio', + 'params': [result.hash, True], + 'id': 5}) - post_data = json.dumps({'method': 'core.set_torrent_stop_ratio', - 'params': [result.hash, float(ratio)], - 'id': 6}) - self._request(method='post', data=post_data) + self._request_json({ + 'method': 'core.set_torrent_stop_ratio', + 'params': [result.hash, float(ratio)], + 'id': 6}) return True def _set_torrent_path(self, result): if sickbeard.TORRENT_PATH: - post_data = json.dumps({ + self._request_json({ 'method': 'core.set_torrent_move_completed', 'params': [result.hash, True], - 'id': 7 - }) - self._request(method='post', data=post_data) + 'id': 7}) - post_data = json.dumps({ + self._request_json({ 'method': 'core.set_torrent_move_completed_path', 'params': [result.hash, sickbeard.TORRENT_PATH], - 'id': 8 - }) - self._request(method='post', data=post_data) + 'id': 8}) return True def _set_torrent_pause(self, result): if sickbeard.TORRENT_PAUSED: - post_data = json.dumps({'method': 'core.pause_torrent', - 'params': [[result.hash]], - 'id': 9}) - self._request(method='post', data=post_data) + self._request_json({ + 'method': 'core.pause_torrent', + 'params': [[result.hash]], + 'id': 9}) return True diff --git a/sickbeard/clients/generic.py b/sickbeard/clients/generic.py index 222f3b9a..3d5b2fc7 100644 --- a/sickbeard/clients/generic.py +++ b/sickbeard/clients/generic.py @@ -56,7 +56,7 @@ class GenericClient(object): return False try: response = self.session.__getattribute__(method)(self.url, params=params, data=data, files=files, - timeout=120, verify=False, **kwargs) + timeout=kwargs.pop('timeout', 120), verify=False, **kwargs) except requests.exceptions.ConnectionError as e: logger.log('%s: Unable to connect %s' % (self.name, ex(e)), logger.ERROR) return False diff --git a/sickbeard/clients/qbittorrent.py b/sickbeard/clients/qbittorrent.py index 51fef677..cdd483d7 100644 --- a/sickbeard/clients/qbittorrent.py +++ b/sickbeard/clients/qbittorrent.py @@ -24,6 +24,7 @@ class QbittorrentAPI(GenericClient): super(QbittorrentAPI, self).__init__('qBittorrent', host, username, password) self.url = self.host + self.session.headers.update({'Origin': self.host}) def _get_auth(self): @@ -38,7 +39,7 @@ class QbittorrentAPI(GenericClient): def _post_api(self, cmd='', **kwargs): - return '' == helpers.getURL('%scommand/%s' % (self.host, cmd), session=self.session, **kwargs) + return helpers.getURL('%scommand/%s' % (self.host, cmd), session=self.session, **kwargs) in ('', 'Ok.') def _add_torrent(self, cmd, **kwargs): diff --git a/sickbeard/common.py b/sickbeard/common.py index 7a3b0522..a13c5309 100644 --- a/sickbeard/common.py +++ b/sickbeard/common.py @@ -67,6 +67,7 @@ SNATCHED_PROPER = 9 # qualified with quality SUBTITLED = 10 # qualified with quality FAILED = 11 # episode downloaded or snatched we don't want SNATCHED_BEST = 12 # episode redownloaded using best quality +SNATCHED_ANY = [SNATCHED, SNATCHED_PROPER, SNATCHED_BEST] NAMING_REPEAT = 1 NAMING_EXTEND = 2 @@ -120,6 +121,31 @@ class Quality: FAILED: 'Failed', SNATCHED_BEST: 'Snatched (Best)'} + real_check = r'\breal\b\W?(?=proper|repack|e?ac3|aac|dts|read\Wnfo|(ws\W)?[ph]dtv|(ws\W)?dsr|web|dvd|blu|\d{2,3}0(p|i))(?!.*\d+(e|x)\d+)' + + proper_levels = [(re.compile(r'\brepack\b(?!.*\d+(e|x)\d+)', flags=re.I), True), + (re.compile(r'\bproper\b(?!.*\d+(e|x)\d+)', flags=re.I), False), + (re.compile(real_check, flags=re.I), False)] + + @staticmethod + def get_proper_level(extra_no_name, version, is_anime=False, check_is_repack=False): + level = 0 + is_repack = False + if is_anime: + if isinstance(version, (int, long)): + level = version + else: + level = 1 + elif isinstance(extra_no_name, basestring): + for p, r_check in Quality.proper_levels: + a = len(p.findall(extra_no_name)) + level += a + if 0 < a and r_check: + is_repack = True + if check_is_repack: + return is_repack, level + return level + @staticmethod def get_quality_css(quality): return (Quality.qualityStrings[quality].replace('2160p', 'UHD2160p').replace('1080p', 'HD1080p') @@ -237,10 +263,16 @@ class Quality: return Quality.FULLHDWEBDL elif checkName(['720p', 'blu.?ray|hddvd|b[r|d]rip', 'x264|h.?264'], all): return Quality.HDBLURAY - elif checkName(['1080p', 'blu.?ray|hddvd|b[r|d]rip', 'x264|h.?264'], all): + elif checkName(['1080p', 'blu.?ray|hddvd|b[r|d]rip', 'x264|h.?264'], all) or \ + (checkName(['1080[pi]', 'remux'], all) and not checkName(['hdtv'], all)): return Quality.FULLHDBLURAY elif checkName(['2160p', 'web.?(dl|rip|.[hx]26[45])'], all): return Quality.UHD4KWEB + # p2p + elif checkName(['720HD'], all) and not checkName(['(1080|2160)[pi]'], all): + return Quality.HDTV + elif checkName(['1080p', 'blu.?ray|hddvd|b[r|d]rip', 'avc|vc[-\s.]?1'], all): + return Quality.FULLHDBLURAY else: return Quality.UNKNOWN @@ -263,7 +295,7 @@ class Quality: logger.log(msg % (filename, e.text), logger.WARNING) except Exception as e: logger.log(msg % (filename, ex(e)), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) if parser: if '.avi' == filename[-4::].lower(): @@ -336,18 +368,22 @@ class Quality: quality = Quality.assumeQuality(file_path) return Quality.compositeStatus(DOWNLOADED, quality) - DOWNLOADED = None SNATCHED = None SNATCHED_PROPER = None - FAILED = None SNATCHED_BEST = None + SNATCHED_ANY = None + DOWNLOADED = None + ARCHIVED = None + FAILED = None -Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] -Quality.FAILED = [Quality.compositeStatus(FAILED, x) for x in Quality.qualityStrings.keys()] Quality.SNATCHED_BEST = [Quality.compositeStatus(SNATCHED_BEST, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED_ANY = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST +Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] +Quality.ARCHIVED = [Quality.compositeStatus(ARCHIVED, x) for x in Quality.qualityStrings.keys()] +Quality.FAILED = [Quality.compositeStatus(FAILED, x) for x in Quality.qualityStrings.keys()] SD = Quality.combineQualities([Quality.SDTV, Quality.SDDVD], []) HD = Quality.combineQualities( @@ -378,29 +414,26 @@ class StatusStrings: self.statusStrings = {UNKNOWN: 'Unknown', UNAIRED: 'Unaired', SNATCHED: 'Snatched', - DOWNLOADED: 'Downloaded', - SKIPPED: 'Skipped', SNATCHED_PROPER: 'Snatched (Proper)', - WANTED: 'Wanted', + SNATCHED_BEST: 'Snatched (Best)', + DOWNLOADED: 'Downloaded', ARCHIVED: 'Archived', + SKIPPED: 'Skipped', + WANTED: 'Wanted', IGNORED: 'Ignored', SUBTITLED: 'Subtitled', - FAILED: 'Failed', - SNATCHED_BEST: 'Snatched (Best)'} + FAILED: 'Failed'} def __getitem__(self, name): - if name in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST: + if name in Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED: status, quality = Quality.splitCompositeStatus(name) if quality == Quality.NONE: return self.statusStrings[status] - else: - return '%s (%s)' % (self.statusStrings[status], Quality.qualityStrings[quality]) - else: - return self.statusStrings[name] if self.statusStrings.has_key(name) else '' + return '%s (%s)' % (self.statusStrings[status], Quality.qualityStrings[quality]) + return self.statusStrings[name] if self.statusStrings.has_key(name) else '' def has_key(self, name): - return name in self.statusStrings or name in Quality.DOWNLOADED or name in Quality.SNATCHED \ - or name in Quality.SNATCHED_PROPER or name in Quality.SNATCHED_BEST + return name in self.statusStrings or name in Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED statusStrings = StatusStrings() @@ -427,3 +460,50 @@ class Overview: countryList = {'Australia': 'AU', 'Canada': 'CA', 'USA': 'US'} + + +class neededQualities: + def __init__(self, need_anime=False, need_sports=False, need_sd=False, need_hd=False, need_uhd=False, + need_webdl=False, need_all_qualities=False, need_all_types=False, need_all=False): + self.need_anime = need_anime or need_all_types or need_all + self.need_sports = need_sports or need_all_types or need_all + self.need_sd = need_sd or need_all_qualities or need_all + self.need_hd = need_hd or need_all_qualities or need_all + self.need_uhd = need_uhd or need_all_qualities or need_all + self.need_webdl = need_webdl or need_all_qualities or need_all + + max_sd = Quality.SDDVD + hd_qualities = [Quality.HDTV, Quality.FULLHDTV, Quality.HDWEBDL, Quality.FULLHDWEBDL, Quality.HDBLURAY, Quality.FULLHDBLURAY] + webdl_qualities = [Quality.SDTV, Quality.HDWEBDL, Quality.FULLHDWEBDL, Quality.UHD4KWEB] + max_hd = Quality.FULLHDBLURAY + + @property + def all_needed(self): + return self.all_qualities_needed and self.all_types_needed + + @property + def all_types_needed(self): + return self.need_anime and self.need_sports + + @property + def all_qualities_needed(self): + return self.need_sd and self.need_hd and self.need_uhd and self.need_webdl + + def check_needed_types(self, show): + if show.is_anime: + self.need_anime = True + if show.is_sports: + self.need_sports = True + + def check_needed_qualities(self, wantedQualities): + if Quality.UNKNOWN in wantedQualities: + self.need_sd = self.need_hd = self.need_uhd = self.need_webdl = True + else: + if not self.need_sd and min(wantedQualities) <= neededQualities.max_sd: + self.need_sd = True + if not self.need_hd and any(i in neededQualities.hd_qualities for i in wantedQualities): + self.need_hd = True + if not self.need_webdl and any(i in neededQualities.webdl_qualities for i in wantedQualities): + self.need_webdl = True + if not self.need_uhd and max(wantedQualities) > neededQualities.max_hd: + self.need_uhd = True diff --git a/sickbeard/config.py b/sickbeard/config.py index f9b17504..d2b68811 100644 --- a/sickbeard/config.py +++ b/sickbeard/config.py @@ -229,15 +229,21 @@ def change_USE_SUBTITLES(use_subtitles): return sickbeard.USE_SUBTITLES = use_subtitles - if sickbeard.USE_SUBTITLES: + if sickbeard.USE_SUBTITLES and not sickbeard.subtitlesFinderScheduler.isAlive(): + sickbeard.subtitlesFinderScheduler = sickbeard.scheduler.Scheduler( + sickbeard.subtitles.SubtitlesFinder(), + cycleTime=datetime.timedelta(hours=sickbeard.SUBTITLES_FINDER_FREQUENCY), + threadName='FINDSUBTITLES', silent=False) sickbeard.subtitlesFinderScheduler.start() else: sickbeard.subtitlesFinderScheduler.stop.set() - logger.log(u'Waiting for the SUBTITLESFINDER thread to exit') + sickbeard.subtitlesFinderScheduler.silent = True + threadname = sickbeard.subtitlesFinderScheduler.name try: sickbeard.subtitlesFinderScheduler.join(10) - except: - pass + logger.log('Thread %s has exit' % threadname) + except RuntimeError: + logger.log('Fail, thread %s did not exit' % threadname) def CheckSection(CFG, sec): @@ -448,7 +454,9 @@ class ConfigMigrator(): 11: 'Migrate anime split view to new layout', 12: 'Add "hevc" and some non-english languages to ignore words if not found', 13: 'Change default dereferrer url to blank', - 14: 'Convert Trakt to multi-account'} + 14: 'Convert Trakt to multi-account', + 15: 'Transmithe.net rebranded Nebulance', + 16: 'Purge old cache image folders'} def migrate_config(self): """ Calls each successive migration until the config is the same version as SG expects """ @@ -783,3 +791,41 @@ class ConfigMigrator(): old_refresh_token = check_setting_str(self.config_obj, 'Trakt', 'trakt_refresh_token', '') if old_token and old_refresh_token: TraktAPI.add_account(old_token, old_refresh_token, None) + + # Migration v15: Transmithe.net variables + def _migrate_v15(self): + try: + neb = filter(lambda p: 'Nebulance' in p.name, sickbeard.providers.sortedProviderList())[0] + except (StandardError, Exception): + return + # get the old settings from the file and store them in the new variable names + old_id = 'transmithe_net' + old_id_uc = old_id.upper() + neb.enabled = bool(check_setting_int(self.config_obj, old_id_uc, old_id, 0)) + setattr(neb, 'username', check_setting_str(self.config_obj, old_id_uc, old_id + '_username', '')) + neb.password = check_setting_str(self.config_obj, old_id_uc, old_id + '_password', '') + neb.minseed = check_setting_int(self.config_obj, old_id_uc, old_id + '_minseed', 0) + neb.minleech = check_setting_int(self.config_obj, old_id_uc, old_id + '_minleech', 0) + neb.freeleech = bool(check_setting_int(self.config_obj, old_id_uc, old_id + '_freeleech', 0)) + neb.enable_recentsearch = bool(check_setting_int( + self.config_obj, old_id_uc, old_id + '_enable_recentsearch', 1)) or not getattr(neb, 'supports_backlog') + neb.enable_backlog = bool(check_setting_int(self.config_obj, old_id_uc, old_id + '_enable_backlog', 1)) + neb.search_mode = check_setting_str(self.config_obj, old_id_uc, old_id + '_search_mode', 'eponly') + neb.search_fallback = bool(check_setting_int(self.config_obj, old_id_uc, old_id + '_search_fallback', 0)) + neb.seed_time = check_setting_int(self.config_obj, old_id_uc, old_id + '_seed_time', '') + neb._seed_ratio = check_setting_str(self.config_obj, old_id_uc, old_id + '_seed_ratio', '') + + # Migration v16: Purge old cache image folder name + @staticmethod + def _migrate_v16(): + if sickbeard.CACHE_DIR and ek.ek(os.path.isdir, sickbeard.CACHE_DIR): + cache_default = sickbeard.CACHE_DIR + dead_paths = ['anidb', 'imdb', 'trakt'] + for path in dead_paths: + sickbeard.CACHE_DIR = '%s/images/%s' % (cache_default, path) + helpers.clearCache(True) + try: + ek.ek(os.rmdir, sickbeard.CACHE_DIR) + except OSError: + pass + sickbeard.CACHE_DIR = cache_default diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index 44f0a5ee..9fd845e2 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -27,7 +27,7 @@ from sickbeard import encodingKludge as ek from sickbeard.name_parser.parser import NameParser, InvalidNameException, InvalidShowException MIN_DB_VERSION = 9 # oldest db version we support migrating from -MAX_DB_VERSION = 20004 +MAX_DB_VERSION = 20006 class MainSanityCheck(db.DBSanityCheck): @@ -37,6 +37,8 @@ class MainSanityCheck(db.DBSanityCheck): self.fix_duplicate_episodes() self.fix_orphan_episodes() self.fix_unaired_episodes() + self.fix_scene_exceptions() + self.fix_orphan_not_found_show() def fix_duplicate_shows(self, column='indexer_id'): @@ -114,28 +116,32 @@ class MainSanityCheck(db.DBSanityCheck): def fix_missing_table_indexes(self): if not self.connection.select('PRAGMA index_info("idx_indexer_id")'): - logger.log(u'Missing idx_indexer_id for TV Shows table detected!, fixing...') - self.connection.action('CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id);') + logger.log('Updating TV Shows table with index idx_indexer_id') + self.connection.action('CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id)') if not self.connection.select('PRAGMA index_info("idx_tv_episodes_showid_airdate")'): - logger.log(u'Missing idx_tv_episodes_showid_airdate for TV Episodes table detected!, fixing...') - self.connection.action('CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes(showid,airdate);') + logger.log('Updating TV Episode table with index idx_tv_episodes_showid_airdate') + self.connection.action('CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes(showid, airdate)') if not self.connection.select('PRAGMA index_info("idx_showid")'): - logger.log(u'Missing idx_showid for TV Episodes table detected!, fixing...') - self.connection.action('CREATE INDEX idx_showid ON tv_episodes (showid);') + logger.log('Updating TV Episode table with index idx_showid') + self.connection.action('CREATE INDEX idx_showid ON tv_episodes (showid)') if not self.connection.select('PRAGMA index_info("idx_status")'): - logger.log(u'Missing idx_status for TV Episodes table detected!, fixing...') - self.connection.action('CREATE INDEX idx_status ON tv_episodes (status,season,episode,airdate)') + logger.log('Updating TV Episode table with index idx_status') + self.connection.action('CREATE INDEX idx_status ON tv_episodes (status, season, episode, airdate)') if not self.connection.select('PRAGMA index_info("idx_sta_epi_air")'): - logger.log(u'Missing idx_sta_epi_air for TV Episodes table detected!, fixing...') - self.connection.action('CREATE INDEX idx_sta_epi_air ON tv_episodes (status,episode, airdate)') + logger.log('Updating TV Episode table with index idx_sta_epi_air') + self.connection.action('CREATE INDEX idx_sta_epi_air ON tv_episodes (status, episode, airdate)') if not self.connection.select('PRAGMA index_info("idx_sta_epi_sta_air")'): - logger.log(u'Missing idx_sta_epi_sta_air for TV Episodes table detected!, fixing...') - self.connection.action('CREATE INDEX idx_sta_epi_sta_air ON tv_episodes (season,episode, status, airdate)') + logger.log('Updating TV Episode table with index idx_sta_epi_sta_air') + self.connection.action('CREATE INDEX idx_sta_epi_sta_air ON tv_episodes (season, episode, status, airdate)') + + if not self.connection.hasIndex('tv_episodes', 'idx_tv_ep_ids'): + logger.log('Updating TV Episode table with index idx_tv_ep_ids') + self.connection.action('CREATE INDEX idx_tv_ep_ids ON tv_episodes (indexer, showid)') def fix_unaired_episodes(self): @@ -159,6 +165,21 @@ class MainSanityCheck(db.DBSanityCheck): else: logger.log(u'No UNAIRED episodes, check passed') + def fix_scene_exceptions(self): + + sql_results = self.connection.select( + 'SELECT exception_id FROM scene_exceptions WHERE season = "null"') + + if 0 < len(sql_results): + logger.log('Fixing invalid scene exceptions') + self.connection.action('UPDATE scene_exceptions SET season = -1 WHERE season = "null"') + + def fix_orphan_not_found_show(self): + sql_result = self.connection.action('DELETE FROM tv_shows_not_found WHERE NOT EXISTS (SELECT NULL FROM ' + 'tv_shows WHERE tv_shows_not_found.indexer == tv_shows.indexer AND ' + 'tv_shows_not_found.indexer_id == tv_shows.indexer_id)') + if sql_result.rowcount: + logger.log('Fixed orphaned not found shows') # ====================== # = Main DB Migrations = @@ -1211,3 +1232,29 @@ class ChangeMapIndexer(db.SchemaUpgrade): self.setDBVersion(20004) return self.checkDBVersion() + + +# 20004 -> 20005 +class AddShowNotFoundCounter(db.SchemaUpgrade): + def execute(self): + if not self.hasTable('tv_shows_not_found'): + logger.log(u'Adding table tv_shows_not_found') + + db.backup_database('sickbeard.db', self.checkDBVersion()) + self.connection.action('CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL, fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC, PRIMARY KEY (indexer_id, indexer))') + + self.setDBVersion(20005) + return self.checkDBVersion() + + +# 20005 -> 20006 +class AddFlagTable(db.SchemaUpgrade): + def execute(self): + if not self.hasTable('flags'): + logger.log(u'Adding table flags') + + db.backup_database('sickbeard.db', self.checkDBVersion()) + self.connection.action('CREATE TABLE flags (flag PRIMARY KEY NOT NULL )') + + self.setDBVersion(20006) + return self.checkDBVersion() diff --git a/sickbeard/db.py b/sickbeard/db.py index a41b09a7..fc9a9637 100644 --- a/sickbeard/db.py +++ b/sickbeard/db.py @@ -47,6 +47,29 @@ def dbFilename(filename='sickbeard.db', suffix=None): return ek.ek(os.path.join, sickbeard.DATA_DIR, filename) +def mass_upsert_sql(tableName, valueDict, keyDict): + + """ + use with cl.extend(mass_upsert_sql(tableName, valueDict, keyDict)) + + :param tableName: table name + :param valueDict: dict of values to be set {'table_fieldname': value} + :param keyDict: dict of restrains for update {'table_fieldname': value} + :return: list of 2 sql command + """ + cl = [] + + genParams = lambda myDict: [x + ' = ?' for x in myDict.keys()] + + cl.append(['UPDATE [%s] SET %s WHERE %s' % ( + tableName, ', '.join(genParams(valueDict)), ' AND '.join(genParams(keyDict))), valueDict.values() + keyDict.values()]) + + + cl.append(['INSERT INTO [' + tableName + '] (' + ', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in valueDict.keys() + keyDict.keys()]) + ')' + + ' SELECT ' + ', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in valueDict.values() + keyDict.values()]) + ' WHERE changes() = 0']) + return cl + + class DBConnection(object): def __init__(self, filename='sickbeard.db', suffix=None, row_type=None): @@ -231,6 +254,20 @@ class DBConnection(object): self.action('ALTER TABLE [%s] ADD %s %s' % (table, column, type)) self.action('UPDATE [%s] SET %s = ?' % (table, column), (default,)) + def has_flag(self, flag_name): + sql_result = self.select('SELECT flag FROM flags WHERE flag = ?', [flag_name]) + if 0 < len(sql_result): + return True + return False + + def add_flag(self, flag_name): + if not self.has_flag(flag_name): + self.action('INSERT INTO flags (flag) VALUES (?)', [flag_name]) + + def remove_flag(self, flag_name): + if self.has_flag(flag_name): + self.action('DELETE FROM flags WHERE flag = ?', [flag_name]) + def close(self): """Close database connection""" if getattr(self, 'connection', None) is not None: @@ -453,7 +490,9 @@ def MigrationCode(myDB): 20000: sickbeard.mainDB.DBIncreaseTo20001, 20001: sickbeard.mainDB.AddTvShowOverview, 20002: sickbeard.mainDB.AddTvShowTags, - 20003: sickbeard.mainDB.ChangeMapIndexer + 20003: sickbeard.mainDB.ChangeMapIndexer, + 20004: sickbeard.mainDB.AddShowNotFoundCounter, + 20005: sickbeard.mainDB.AddFlagTable # 20002: sickbeard.mainDB.AddCoolSickGearFeature3, } diff --git a/sickbeard/encodingKludge.py b/sickbeard/encodingKludge.py index 95012c11..e9ec28b7 100644 --- a/sickbeard/encodingKludge.py +++ b/sickbeard/encodingKludge.py @@ -59,11 +59,18 @@ def callPeopleStupid(x): return x.encode(sickbeard.SYS_ENCODING, 'ignore') +def fixParaLists(x): + if type(x) == list: + return [callPeopleStupid(a) if type(a) in (str, unicode) else a for a in x] + else: + return x + + def ek(func, *args, **kwargs): if os.name == 'nt': result = func(*args, **kwargs) else: - result = func(*[callPeopleStupid(x) if type(x) in (str, unicode) else x for x in args], **kwargs) + result = func(*[callPeopleStupid(x) if type(x) in (str, unicode) else fixParaLists(x) for x in args], **kwargs) if type(result) in (list, tuple): return fixListEncodings(result) diff --git a/sickbeard/failed_history.py b/sickbeard/failed_history.py index c7f09718..f2db0c05 100644 --- a/sickbeard/failed_history.py +++ b/sickbeard/failed_history.py @@ -1,5 +1,4 @@ # Author: Tyler Fenby -# URL: http://code.google.com/p/sickbeard/ # # This file is part of SickGear. # @@ -16,26 +15,41 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import datetime import re import urllib -import datetime -from sickbeard import db -from sickbeard import logger -from sickbeard.exceptions import ex, EpisodeNotFoundException +from sickbeard import db, logger +from sickbeard.common import FAILED, WANTED, Quality, statusStrings +from sickbeard.exceptions import EpisodeNotFoundException, ex from sickbeard.history import dateFormat -from sickbeard.common import Quality -from sickbeard.common import WANTED, FAILED -def prepareFailedName(release): +def db_cmd(sql, params, select=True): + + my_db = db.DBConnection('failed.db') + sql_result = select and my_db.select(sql, params) or my_db.action(sql, params) + return sql_result + + +def db_select(sql, params): + + return db_cmd(sql, params) + + +def db_action(sql, params=None): + + return db_cmd(sql, params, False) + + +def prepare_failed_name(release): """Standardizes release name for failed DB""" fixed = urllib.unquote(release) - if (fixed.endswith(".nzb")): - fixed = fixed.rpartition(".")[0] + if fixed.endswith('.nzb'): + fixed = fixed.rpartition('.')[0] - fixed = re.sub("[\.\-\+\ ]", "_", fixed) + fixed = re.sub('[.\-+ ]', '_', fixed) if not isinstance(fixed, unicode): fixed = unicode(fixed, 'utf-8', 'replace') @@ -43,57 +57,96 @@ def prepareFailedName(release): return fixed -def logFailed(release): - log_str = u"" +def add_failed(release): + size = -1 - provider = "" + provider = '' - release = prepareFailedName(release) + release = prepare_failed_name(release) - myDB = db.DBConnection('failed.db') - sql_results = myDB.select("SELECT * FROM history WHERE release=?", [release]) + sql_results = db_select('SELECT * FROM history t WHERE t.release=?', [release]) + + if not any(sql_results): + logger.log('Release not found in failed.db snatch history', logger.WARNING) + + elif 1 < len(sql_results): + logger.log('Multiple logged snatches found for release in failed.db', logger.WARNING) + sizes = len(set(x['size'] for x in sql_results)) + providers = len(set(x['provider'] for x in sql_results)) + + if 1 == sizes: + logger.log('However, they\'re all the same size. Continuing with found size', logger.WARNING) + size = sql_results[0]['size'] - if len(sql_results) == 0: - logger.log( - u"Release not found in snatch history.", logger.WARNING) - elif len(sql_results) > 1: - logger.log(u"Multiple logged snatches found for release", logger.WARNING) - sizes = len(set(x["size"] for x in sql_results)) - providers = len(set(x["provider"] for x in sql_results)) - if sizes == 1: - logger.log(u"However, they're all the same size. Continuing with found size.", logger.WARNING) - size = sql_results[0]["size"] else: logger.log( - u"They also vary in size. Deleting the logged snatches and recording this release with no size/provider", + 'They also vary in size. Deleting logged snatches and recording this release with no size/provider', logger.WARNING) for result in sql_results: - deleteLoggedSnatch(result["release"], result["size"], result["provider"]) + remove_snatched(result['release'], result['size'], result['provider']) - if providers == 1: - logger.log(u"They're also from the same provider. Using it as well.") - provider = sql_results[0]["provider"] + if 1 == providers: + logger.log('They\'re also from the same provider. Using it as well') + provider = sql_results[0]['provider'] else: - size = sql_results[0]["size"] - provider = sql_results[0]["provider"] + size = sql_results[0]['size'] + provider = sql_results[0]['provider'] - if not hasFailed(release, size, provider): - myDB = db.DBConnection('failed.db') - myDB.action("INSERT INTO failed (release, size, provider) VALUES (?, ?, ?)", [release, size, provider]) + if not has_failed(release, size, provider): + db_action('INSERT INTO failed (`release`, `size`, `provider`) VALUES (?, ?, ?)', [release, size, provider]) - deleteLoggedSnatch(release, size, provider) - - return log_str + remove_snatched(release, size, provider) -def logSuccess(release): - release = prepareFailedName(release) +def add_snatched(search_result): + """ + :param search_result: SearchResult object + """ - myDB = db.DBConnection('failed.db') - myDB.action("DELETE FROM history WHERE release=?", [release]) + log_date = datetime.datetime.today().strftime(dateFormat) + + provider = 'unknown' + if None is not search_result.provider: + provider = search_result.provider.name + + for episode in search_result.episodes: + db_action( + 'INSERT INTO history (`date`, `size`, `release`, `provider`, `showid`, `season`, `episode`, `old_status`)' + 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [log_date, search_result.size, prepare_failed_name(search_result.name), provider, + search_result.episodes[0].show.indexerid, episode.season, episode.episode, episode.status]) -def hasFailed(release, size, provider="%"): +def set_episode_failed(ep_obj): + + try: + with ep_obj.lock: + quality = Quality.splitCompositeStatus(ep_obj.status)[1] + ep_obj.status = Quality.compositeStatus(FAILED, quality) + ep_obj.saveToDB() + + except EpisodeNotFoundException as e: + logger.log('Unable to get episode, please set its status manually: %s' % ex(e), logger.WARNING) + + +def remove_failed(release): + + db_action('DELETE FROM history WHERE %s=?' % '`release`', [prepare_failed_name(release)]) + + +def remove_snatched(release, size, provider): + + db_action('DELETE FROM history WHERE %s=? AND %s=? AND %s=?' % ('`release`', '`size`', '`provider`'), + [prepare_failed_name(release), size, provider]) + + +def remove_old_history(): + + db_action('DELETE FROM history WHERE date < %s' % + str((datetime.datetime.today() - datetime.timedelta(days=30)).strftime(dateFormat))) + + +def has_failed(release, size, provider='%'): """ Returns True if a release has previously failed. @@ -101,125 +154,83 @@ def hasFailed(release, size, provider="%"): with that specific provider. Otherwise, return True if the release is found with any provider. """ - - release = prepareFailedName(release) - - myDB = db.DBConnection('failed.db') - sql_results = myDB.select( - "SELECT * FROM failed WHERE release=? AND size=? AND provider LIKE ?", - [release, size, provider]) - - return (len(sql_results) > 0) + return any(db_select('SELECT * FROM failed t WHERE t.release=? AND t.size=? AND t.provider LIKE ?', + [prepare_failed_name(release), size, provider])) -def revertEpisode(epObj): +def revert_episode(ep_obj): """Restore the episodes of a failed download to their original state""" - myDB = db.DBConnection('failed.db') - sql_results = myDB.select("SELECT * FROM history WHERE showid=? AND season=?", - [epObj.show.indexerid, epObj.season]) + sql_results = db_select( + 'SELECT * FROM history t WHERE t.showid=? AND t.season=?', [ep_obj.show.indexerid, ep_obj.season]) - history_eps = dict([(res["episode"], res) for res in sql_results]) + history_eps = {r['episode']: r for r in sql_results} try: - logger.log(u"Reverting episode (%s, %s): %s" % (epObj.season, epObj.episode, epObj.name)) - with epObj.lock: - if epObj.episode in history_eps: - logger.log(u"Found in history") - epObj.status = history_eps[epObj.episode]['old_status'] + logger.log('Reverting episode %sx%s: [%s]' % (ep_obj.season, ep_obj.episode, ep_obj.name)) + with ep_obj.lock: + if ep_obj.episode in history_eps: + status_revert = history_eps[ep_obj.episode]['old_status'] + + status, quality = Quality.splitCompositeStatus(status_revert) + logger.log('Found in failed.db history with status: %s quality: %s' % ( + statusStrings[status], Quality.qualityStrings[quality])) else: - logger.log(u"WARNING: Episode not found in history. Setting it back to WANTED", - logger.WARNING) - epObj.status = WANTED - epObj.saveToDB() + status_revert = WANTED + + logger.log('Episode not found in failed.db history. Setting it to WANTED', logger.WARNING) + + ep_obj.status = status_revert + ep_obj.saveToDB() except EpisodeNotFoundException as e: - logger.log(u"Unable to create episode, please set its status manually: " + ex(e), - logger.WARNING) + logger.log('Unable to create episode, please set its status manually: %s' % ex(e), logger.WARNING) -def markFailed(epObj): - log_str = u"" - - try: - with epObj.lock: - quality = Quality.splitCompositeStatus(epObj.status)[1] - epObj.status = Quality.compositeStatus(FAILED, quality) - epObj.saveToDB() - - except EpisodeNotFoundException as e: - logger.log(u"Unable to get episode, please set its status manually: " + ex(e), logger.WARNING) - - return log_str +def find_old_status(ep_obj): + """ + :param ep_obj: + :return: Old status if failed history item found + """ + # Search for release in snatch history + results = db_select( + 'SELECT t.old_status FROM history t WHERE t.showid=? AND t.season=? AND t.episode=? ' + + 'ORDER BY t.date DESC LIMIT 1', [ep_obj.show.indexerid, ep_obj.season, ep_obj.episode]) + if any(results): + return results[0]['old_status'] -def logSnatch(searchResult): - logDate = datetime.datetime.today().strftime(dateFormat) - release = prepareFailedName(searchResult.name) - - providerClass = searchResult.provider - if providerClass is not None: - provider = providerClass.name - else: - provider = "unknown" - - show_obj = searchResult.episodes[0].show - - myDB = db.DBConnection('failed.db') - for episode in searchResult.episodes: - myDB.action( - "INSERT INTO history (date, size, release, provider, showid, season, episode, old_status)" - "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [logDate, searchResult.size, release, provider, show_obj.indexerid, episode.season, episode.episode, - episode.status]) - - -def deleteLoggedSnatch(release, size, provider): - release = prepareFailedName(release) - - myDB = db.DBConnection('failed.db') - myDB.action("DELETE FROM history WHERE release=? AND size=? AND provider=?", - [release, size, provider]) - - -def trimHistory(): - myDB = db.DBConnection('failed.db') - myDB.action("DELETE FROM history WHERE date < " + str( - (datetime.datetime.today() - datetime.timedelta(days=30)).strftime(dateFormat))) - - -def findRelease(epObj): +def find_release(ep_obj): """ Find releases in history by show ID and season. Return None for release if multiple found or no release found. """ - - release = None - provider = None - - # Clear old snatches for this release if any exist - myDB = db.DBConnection('failed.db') - myDB.action("DELETE FROM history WHERE showid=" + str(epObj.show.indexerid) + " AND season=" + str( - epObj.season) + " AND episode=" + str( - epObj.episode) + " AND date < (SELECT max(date) FROM history WHERE showid=" + str( - epObj.show.indexerid) + " AND season=" + str(epObj.season) + " AND episode=" + str(epObj.episode) + ")") + from_where = ' FROM history WHERE showid=%s AND season=%s AND episode=%s' % \ + (ep_obj.show.indexerid, ep_obj.season, ep_obj.episode) + db_action('DELETE %s AND date < (SELECT max(date) %s)' % (from_where, from_where)) # Search for release in snatch history - results = myDB.select("SELECT release, provider, date FROM history WHERE showid=? AND season=? AND episode=?", - [epObj.show.indexerid, epObj.season, epObj.episode]) + results = db_select( + 'SELECT t.release, t.provider, t.date FROM history t WHERE t.showid=? AND t.season=? AND t.episode=?', + [ep_obj.show.indexerid, ep_obj.season, ep_obj.episode]) - for result in results: - release = str(result["release"]) - provider = str(result["provider"]) - date = result["date"] + if any(results): + r = results[0] + release = r['release'] + provider = r['provider'] # Clear any incomplete snatch records for this release if any exist - myDB.action("DELETE FROM history WHERE release=? AND date!=?", [release, date]) + db_action('DELETE FROM history WHERE %s=? AND %s!=?' % ('`release`', '`date`'), [release, r['date']]) # Found a previously failed release - logger.log(u"Failed release found for season (%s): (%s)" % (epObj.season, result["release"]), logger.DEBUG) - return (release, provider) + logger.log('Found failed.db history release %sx%s: [%s]' % ( + ep_obj.season, ep_obj.episode, release), logger.DEBUG) + else: + release = None + provider = None - # Release was not found - logger.log(u"No releases found for season (%s) of (%s)" % (epObj.season, epObj.show.indexerid), logger.DEBUG) - return (release, provider) \ No newline at end of file + # Release not found + logger.log('No found failed.db history release for %sx%s: [%s]' % ( + ep_obj.season, ep_obj.episode, ep_obj.show.name), logger.DEBUG) + + return release, provider diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index d1156cda..c9124fe3 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -34,10 +34,12 @@ import traceback import urlparse import uuid import subprocess +import sys import adba import requests import requests.exceptions +from cfscrape import CloudflareScraper import sickbeard import subliminal @@ -53,7 +55,8 @@ except ImportError: from sickbeard.exceptions import MultipleShowObjectsException, ex from sickbeard import logger, db, notifiers, clients -from sickbeard.common import USER_AGENT, mediaExtensions, subtitleExtensions, cpu_presets +from sickbeard.common import USER_AGENT, mediaExtensions, subtitleExtensions, cpu_presets, statusStrings, \ + SNATCHED_ANY, DOWNLOADED, ARCHIVED, IGNORED, Quality from sickbeard import encodingKludge as ek from lib.cachecontrol import CacheControl, caches @@ -222,7 +225,7 @@ def makeDir(path): try: ek.ek(os.makedirs, path) # do the library update for synoindex - notifiers.synoindex_notifier.addFolder(path) + notifiers.NotifierFactory().get('SYNOINDEX').addFolder(path) except OSError: return False return True @@ -298,7 +301,7 @@ def listMediaFiles(path): def copyFile(srcFile, destFile): if os.name.startswith('posix'): - subprocess.call(['cp', srcFile, destFile]) + ek.ek(subprocess.call, ['cp', srcFile, destFile]) else: ek.ek(shutil.copyfile, srcFile, destFile) @@ -392,7 +395,7 @@ def make_dirs(path): # use normpath to remove end separator, otherwise checks permissions against itself chmodAsParent(ek.ek(os.path.normpath, sofar)) # do the library update for synoindex - notifiers.synoindex_notifier.addFolder(sofar) + notifiers.NotifierFactory().get('SYNOINDEX').addFolder(sofar) except (OSError, IOError) as e: logger.log(u'Failed creating %s : %s' % (sofar, ex(e)), logger.ERROR) return False @@ -475,7 +478,7 @@ def delete_empty_folders(check_empty_dir, keep_dir=None): # need shutil.rmtree when ignore_items is really implemented ek.ek(os.rmdir, check_empty_dir) # do the library update for synoindex - notifiers.synoindex_notifier.deleteFolder(check_empty_dir) + notifiers.NotifierFactory().get('SYNOINDEX').deleteFolder(check_empty_dir) except OSError as e: logger.log(u"Unable to delete " + check_empty_dir + ": " + repr(e) + " / " + str(e), logger.WARNING) break @@ -613,7 +616,7 @@ def sanitizeSceneName(name): """ if name: - bad_chars = u",:()'!?\u2019" + bad_chars = u",:()£'!?\u2019" # strip out any bad chars for x in bad_chars: @@ -644,12 +647,12 @@ def create_https_certificates(ssl_cert, ssl_key): return False # Create the CA Certificate - cakey = createKeyPair(TYPE_RSA, 1024) + cakey = createKeyPair(TYPE_RSA, 4096) careq = createCertRequest(cakey, CN='Certificate Authority') cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years cname = 'SickGear' - pkey = createKeyPair(TYPE_RSA, 1024) + pkey = createKeyPair(TYPE_RSA, 4096) req = createCertRequest(pkey, CN=cname) cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years @@ -1015,15 +1018,14 @@ def set_up_anidb_connection(): return sickbeard.ADBA_CONNECTION.authed() -def touchFile(fname, atime=None): - if None != atime: +def touch_file(fname, atime=None): + if None is not atime: try: with open(fname, 'a'): ek.ek(os.utime, fname, (atime, atime)) - return True - except: - logger.log(u"File air date stamping not available on your OS", logger.DEBUG) - pass + return True + except (StandardError, Exception): + logger.log('File air date stamping not available on your OS', logger.DEBUG) return False @@ -1099,46 +1101,61 @@ def proxy_setting(proxy_setting, request_url, force=False): return (False, proxy_address)[request_url_match], True -def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=None, json=False, raise_status_code=False, **kwargs): +def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=None, json=False, + raise_status_code=False, raise_exceptions=False, **kwargs): """ - Returns a byte-string retrieved from the url provider. + Either + 1) Returns a byte-string retrieved from the url provider. + 2) Return True/False if success after using kwargs 'savefile' set to file pathname. """ - # request session - if None is session: - session = requests.session() - - if not kwargs.get('nocache'): - cache_dir = sickbeard.CACHE_DIR or _getTempDir() - session = CacheControl(sess=session, cache=caches.FileCache(ek.ek(os.path.join, cache_dir, 'sessions'))) - else: - del(kwargs['nocache']) - - # request session headers - req_headers = {'User-Agent': USER_AGENT, 'Accept-Encoding': 'gzip,deflate'} - if headers: - req_headers.update(headers) - session.headers.update(req_headers) - + # selectively mute some errors mute = [] for muted in filter( lambda x: kwargs.get(x, False), ['mute_connect_err', 'mute_read_timeout', 'mute_connect_timeout']): mute += [muted] del kwargs[muted] - # request session ssl verify - session.verify = False + # reuse or instantiate request session + if None is session: + session = CloudflareScraper.create_scraper() - # request session paramaters + # download and save file or simply fetch url + savename = None + if 'savename' in kwargs: + # session streaming + session.stream = True + savename = kwargs.pop('savename') + + if 'nocache' in kwargs: + del kwargs['nocache'] + else: + cache_dir = sickbeard.CACHE_DIR or _getTempDir() + session = CacheControl(sess=session, cache=caches.FileCache(ek.ek(os.path.join, cache_dir, 'sessions'))) + + # session master headers + req_headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Encoding': 'gzip,deflate', 'User-Agent': USER_AGENT} + if headers: + req_headers.update(headers) + if hasattr(session, 'reserved') and 'headers' in session.reserved: + req_headers.update(session.reserved['headers'] or {}) + session.headers.update(req_headers) + + # session paramaters session.params = params + # session ssl verify + session.verify = False + + response = None try: - # Remove double-slashes from url + # sanitise url parsed = list(urlparse.urlparse(url)) - parsed[2] = re.sub("/{2,}", "/", parsed[2]) # replace two or more / with one + parsed[2] = re.sub('/{2,}', '/', parsed[2]) # replace two or more / with one url = urlparse.urlunparse(parsed) - # request session proxies + # session proxies if sickbeard.PROXY_SETTING: (proxy_address, pac_found) = proxy_setting(sickbeard.PROXY_SETTING, url) msg = '%sproxy for url: %s' % (('', 'PAC parsed ')[pac_found], url) @@ -1147,46 +1164,45 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N return elif proxy_address: logger.log('Using %s' % msg, logger.DEBUG) - session.proxies = { - 'http': proxy_address, - 'https': proxy_address - } + session.proxies = {'http': proxy_address, 'https': proxy_address} # decide if we get or post data to server if 'post_json' in kwargs: - kwargs.setdefault('json', kwargs.get('post_json')) - del(kwargs['post_json']) + kwargs.setdefault('json', kwargs.pop('post_json')) + if post_data: kwargs.setdefault('data', post_data) + if 'data' in kwargs or 'json' in kwargs: - resp = session.post(url, timeout=timeout, **kwargs) + response = session.post(url, timeout=timeout, **kwargs) else: - resp = session.get(url, timeout=timeout, **kwargs) - if resp.ok and not resp.content and 'url=' in resp.headers.get('Refresh', '').lower(): - url = resp.headers.get('Refresh').lower().split('url=')[1].strip('/') + response = session.get(url, timeout=timeout, **kwargs) + if response.ok and not response.content and 'url=' in response.headers.get('Refresh', '').lower(): + url = response.headers.get('Refresh').lower().split('url=')[1].strip('/') if not url.startswith('http'): parsed[2] = '/%s' % url url = urlparse.urlunparse(parsed) - resp = session.get(url, timeout=timeout, **kwargs) + response = session.get(url, timeout=timeout, **kwargs) if raise_status_code: - resp.raise_for_status() + response.raise_for_status() - if not resp.ok: - http_err_text = 'CloudFlare Ray ID' in resp.content and 'CloudFlare reports, "Website is offline"; ' or '' - if resp.status_code in clients.http_error_code: - http_err_text += clients.http_error_code[resp.status_code] - elif resp.status_code in range(520, 527): + if not response.ok: + http_err_text = 'CloudFlare Ray ID' in response.content and \ + 'CloudFlare reports, "Website is offline"; ' or '' + if response.status_code in clients.http_error_code: + http_err_text += clients.http_error_code[response.status_code] + elif response.status_code in range(520, 527): http_err_text += 'Origin server connection failure' else: http_err_text = 'Custom HTTP error code' logger.log(u'Response not ok. %s: %s from requested url %s' - % (resp.status_code, http_err_text, url), logger.DEBUG) + % (response.status_code, http_err_text, url), logger.DEBUG) return except requests.exceptions.HTTPError as e: if raise_status_code: - resp.raise_for_status() + response.raise_for_status() logger.log(u'HTTP error %s while loading URL%s' % ( e.errno, _maybe_request_url(e)), logger.WARNING) return @@ -1194,16 +1210,22 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N if 'mute_connect_err' not in mute: logger.log(u'Connection error msg:%s while loading URL%s' % ( e.message, _maybe_request_url(e)), logger.WARNING) + if raise_exceptions: + raise e return except requests.exceptions.ReadTimeout as e: if 'mute_read_timeout' not in mute: logger.log(u'Read timed out msg:%s while loading URL%s' % ( e.message, _maybe_request_url(e)), logger.WARNING) + if raise_exceptions: + raise e return except (requests.exceptions.Timeout, socket.timeout) as e: if 'mute_connect_timeout' not in mute: logger.log(u'Connection timed out msg:%s while loading URL %s' % ( e.message, _maybe_request_url(e, url)), logger.WARNING) + if raise_exceptions: + raise e return except Exception as e: if e.message: @@ -1212,88 +1234,50 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N else: logger.log(u'Unknown exception while loading URL %s\r\nDetail... %s' % (url, traceback.format_exc()), logger.WARNING) + if raise_exceptions: + raise e return if json: try: - return resp.json() + data_json = response.json() + return ({}, data_json)[isinstance(data_json, (dict, list))] except (TypeError, Exception) as e: logger.log(u'JSON data issue from URL %s\r\nDetail... %s' % (url, e.message), logger.WARNING) + if raise_exceptions: + raise e return None - return resp.content + if savename: + try: + with open(savename, 'wb') as fp: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + fp.write(chunk) + fp.flush() + ek.ek(os.fsync, fp.fileno()) + + chmodAsParent(savename) + + except EnvironmentError as e: + logger.log(u'Unable to save the file: ' + ex(e), logger.ERROR) + if raise_exceptions: + raise e + return + return True + + return response.content def _maybe_request_url(e, def_url=''): return hasattr(e, 'request') and hasattr(e.request, 'url') and ' ' + e.request.url or def_url -def download_file(url, filename, session=None): - # create session - if None is session: - session = requests.session() - cache_dir = sickbeard.CACHE_DIR or _getTempDir() - session = CacheControl(sess=session, cache=caches.FileCache(ek.ek(os.path.join, cache_dir, 'sessions'))) +def download_file(url, filename, session=None, **kwargs): - # request session headers - session.headers.update({'User-Agent': USER_AGENT, 'Accept-Encoding': 'gzip,deflate'}) - - # request session ssl verify - session.verify = False - - # request session streaming - session.stream = True - - # request session proxies - if sickbeard.PROXY_SETTING: - (proxy_address, pac_found) = proxy_setting(sickbeard.PROXY_SETTING, url) - msg = '%sproxy for url: %s' % (('', 'PAC parsed ')[pac_found], url) - if None is proxy_address: - logger.log('Proxy error, aborted the request using %s' % msg, logger.DEBUG) - return - elif proxy_address: - logger.log('Using %s' % msg, logger.DEBUG) - session.proxies = { - 'http': proxy_address, - 'https': proxy_address - } - - try: - resp = session.get(url) - if not resp.ok: - logger.log(u"Requested url " + url + " returned status code is " + str( - resp.status_code) + ': ' + clients.http_error_code[resp.status_code], logger.DEBUG) - return False - - with open(filename, 'wb') as fp: - for chunk in resp.iter_content(chunk_size=1024): - if chunk: - fp.write(chunk) - fp.flush() - ek.ek(os.fsync, fp.fileno()) - - chmodAsParent(filename) - except requests.exceptions.HTTPError as e: + if None is getURL(url, session=session, savename=filename, **kwargs): remove_file_failed(filename) - logger.log(u"HTTP error " + str(e.errno) + " while loading URL " + url, logger.WARNING) return False - except requests.exceptions.ConnectionError as e: - remove_file_failed(filename) - logger.log(u"Connection error " + str(e.message) + " while loading URL " + url, logger.WARNING) - return False - except requests.exceptions.Timeout as e: - remove_file_failed(filename) - logger.log(u"Connection timed out " + str(e.message) + " while loading URL " + url, logger.WARNING) - return False - except EnvironmentError as e: - remove_file_failed(filename) - logger.log(u"Unable to save the file: " + ex(e), logger.ERROR) - return False - except Exception: - remove_file_failed(filename) - logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING) - return False - return True @@ -1351,12 +1335,10 @@ def human(size): def get_size(start_path='.'): if ek.ek(os.path.isfile, start_path): return ek.ek(os.path.getsize, start_path) - total_size = 0 - for dirpath, dirnames, filenames in ek.ek(os.walk, start_path): - for f in filenames: - fp = ek.ek(os.path.join, dirpath, f) - total_size += ek.ek(os.path.getsize, fp) - return total_size + try: + return sum(map((lambda x: x.stat(follow_symlinks=False).st_size), scantree(start_path))) + except OSError: + return 0 def remove_article(text=''): @@ -1495,8 +1477,8 @@ def cleanup_cache(): """ Delete old cached files """ - delete_not_changed_in([ek.ek(os.path.join, sickbeard.CACHE_DIR, *x) for x in [ - ('images', 'trakt'), ('images', 'imdb'), ('images', 'anidb')]]) + delete_not_changed_in([ek.ek(os.path.join, sickbeard.CACHE_DIR, 'images', 'browse', 'thumb', x) for x in [ + 'anidb', 'imdb', 'trakt', 'tvdb']]) def delete_not_changed_in(paths, days=30, minutes=0): @@ -1534,3 +1516,42 @@ def set_file_timestamp(filename, min_age=3, new_time=None): ek.ek(os.utime, filename, new_time) except (StandardError, Exception): pass + + +def should_delete_episode(status): + s = Quality.splitCompositeStatus(status) + if s not in SNATCHED_ANY + [DOWNLOADED, ARCHIVED, IGNORED]: + return True + logger.log('not safe to delete episode from db because of status: %s' % statusStrings[s], logger.DEBUG) + return False + + +def is_link(filepath): + """ + Check if given file/pathname is symbolic link + + :param filepath: file or path to check + :return: True or False + """ + if 'win32' == sys.platform: + if not ek.ek(os.path.exists, filepath): + return False + + import ctypes + invalid_file_attributes = 0xFFFFFFFF + file_attribute_reparse_point = 0x0400 + + attr = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath)) + return invalid_file_attributes != attr and 0 != attr & file_attribute_reparse_point + + return ek.ek(os.path.islink, filepath) + + +def datetime_to_epoch(dt): + """ convert a datetime to seconds after (or possibly before) 1970-1-1 """ + """ can raise an error with dates pre 1970-1-1 """ + if not isinstance(getattr(dt, 'tzinfo'), datetime.tzinfo): + from sickbeard.network_timezones import sb_timezone + dt = dt.replace(tzinfo=sb_timezone) + utc_naive = dt.replace(tzinfo=None) - dt.utcoffset() + return int((utc_naive - datetime.datetime(1970, 1, 1)).total_seconds()) diff --git a/sickbeard/history.py b/sickbeard/history.py index 7bc907ca..c25f63e0 100644 --- a/sickbeard/history.py +++ b/sickbeard/history.py @@ -19,50 +19,55 @@ import db import datetime -from sickbeard.common import SNATCHED, SUBTITLED, FAILED, Quality +import sickbeard +from sickbeard import helpers, logger +from sickbeard.common import SNATCHED, SNATCHED_PROPER, SUBTITLED, FAILED, Quality +from sickbeard.name_parser.parser import NameParser -dateFormat = "%Y%m%d%H%M%S" +dateFormat = '%Y%m%d%H%M%S' -def _logHistoryItem(action, showid, season, episode, quality, resource, provider, version=-1): - logDate = datetime.datetime.today().strftime(dateFormat) +def _log_history_item(action, showid, season, episode, quality, resource, provider, version=-1): + log_date = datetime.datetime.today().strftime(dateFormat) if not isinstance(resource, unicode): resource = unicode(resource, 'utf-8', 'replace') - myDB = db.DBConnection() - myDB.action( - "INSERT INTO history (action, date, showid, season, episode, quality, resource, provider, version) VALUES (?,?,?,?,?,?,?,?,?)", - [action, logDate, showid, season, episode, quality, resource, provider, version]) + my_db = db.DBConnection() + my_db.action( + 'INSERT INTO history (action, date, showid, season, episode, quality, resource, provider, version)' + ' VALUES (?,?,?,?,?,?,?,?,?)', + [action, log_date, showid, season, episode, quality, resource, provider, version]) -def logSnatch(searchResult): - for curEpObj in searchResult.episodes: +def log_snatch(search_result): + for curEpObj in search_result.episodes: showid = int(curEpObj.show.indexerid) season = int(curEpObj.season) episode = int(curEpObj.episode) - quality = searchResult.quality - version = searchResult.version + quality = search_result.quality + version = search_result.version + is_proper = 0 < search_result.properlevel - providerClass = searchResult.provider - if providerClass != None: - provider = providerClass.name + provider_class = search_result.provider + if None is not provider_class: + provider = provider_class.name else: - provider = "unknown" + provider = 'unknown' - action = Quality.compositeStatus(SNATCHED, searchResult.quality) + action = Quality.compositeStatus((SNATCHED, SNATCHED_PROPER)[is_proper], search_result.quality) - resource = searchResult.name + resource = search_result.name - _logHistoryItem(action, showid, season, episode, quality, resource, provider, version) + _log_history_item(action, showid, season, episode, quality, resource, provider, version) -def logDownload(episode, filename, new_ep_quality, release_group=None, version=-1): +def log_download(episode, filename, new_ep_quality, release_group=None, version=-1): showid = int(episode.show.indexerid) season = int(episode.season) - epNum = int(episode.episode) + ep_num = int(episode.episode) quality = new_ep_quality @@ -74,53 +79,87 @@ def logDownload(episode, filename, new_ep_quality, release_group=None, version=- action = episode.status - _logHistoryItem(action, showid, season, epNum, quality, filename, provider, version) + _log_history_item(action, showid, season, ep_num, quality, filename, provider, version) -def logSubtitle(showid, season, episode, status, subtitleResult): - resource = subtitleResult.path - provider = subtitleResult.service +def log_subtitle(showid, season, episode, status, subtitle_result): + resource = subtitle_result.path + provider = subtitle_result.service status, quality = Quality.splitCompositeStatus(status) action = Quality.compositeStatus(SUBTITLED, quality) - _logHistoryItem(action, showid, season, episode, quality, resource, provider) + _log_history_item(action, showid, season, episode, quality, resource, provider) -def logFailed(epObj, release, provider=None): - showid = int(epObj.show.indexerid) - season = int(epObj.season) - epNum = int(epObj.episode) - status, quality = Quality.splitCompositeStatus(epObj.status) +def log_failed(ep_obj, release, provider=None): + showid = int(ep_obj.show.indexerid) + season = int(ep_obj.season) + ep_num = int(ep_obj.episode) + status, quality = Quality.splitCompositeStatus(ep_obj.status) action = Quality.compositeStatus(FAILED, quality) - _logHistoryItem(action, showid, season, epNum, quality, release, provider) + _log_history_item(action, showid, season, ep_num, quality, release, provider) def reset_status(indexerid, season, episode): - ''' Revert episode history to status from download history, - if history exists ''' + """ Revert episode history to status from download history, + if history exists """ my_db = db.DBConnection() - history_sql = 'SELECT h.action, h.showid, h.season, h.episode,'\ - ' t.status FROM history AS h INNER JOIN tv_episodes AS t'\ - ' ON h.showid = t.showid AND h.season = t.season'\ - ' AND h.episode = t.episode WHERE t.showid = ? AND t.season = ?'\ - ' AND t.episode = ? GROUP BY h.action ORDER BY h.date DESC limit 1' + history_sql = 'SELECT h.action, h.showid, h.season, h.episode, t.status' \ + ' FROM history AS h' \ + ' INNER JOIN tv_episodes AS t' \ + ' ON h.showid = t.showid AND h.season = t.season AND h.episode = t.episode' \ + ' WHERE t.showid = ? AND t.season = ? AND t.episode = ?' \ + ' GROUP BY h.action' \ + ' ORDER BY h.date DESC' \ + ' LIMIT 1' - sql_history = my_db.select(history_sql, [str(indexerid), - str(season), - str(episode)]) - if len(sql_history) == 1: + sql_history = my_db.select(history_sql, [str(indexerid), str(season), str(episode)]) + if 1 == len(sql_history): history = sql_history[0] # update status only if status differs # FIXME: this causes issues if the user changed status manually # replicating refactored behavior anyway. if history['status'] != history['action']: - undo_status = 'UPDATE tv_episodes SET status = ?'\ - ' WHERE showid = ? AND season = ? AND episode = ?' + undo_status = 'UPDATE tv_episodes SET status = ?' \ + ' WHERE showid = ? AND season = ? AND episode = ?' my_db.action(undo_status, [history['action'], history['showid'], history['season'], history['episode']]) + + +def history_snatched_proper_fix(): + my_db = db.DBConnection() + if not my_db.has_flag('history_snatch_proper'): + logger.log('Updating history items with status Snatched Proper in a background process...') + sql_result = my_db.select('SELECT rowid, resource, quality, showid' + ' FROM history' + ' WHERE action LIKE "%%%02d"' % SNATCHED + + ' AND (UPPER(resource) LIKE "%PROPER%"' + ' OR UPPER(resource) LIKE "%REPACK%"' + ' OR UPPER(resource) LIKE "%REAL%")') + if sql_result: + cl = [] + for r in sql_result: + show_obj = None + try: + show_obj = helpers.findCertainShow(sickbeard.showList, int(r['showid'])) + except (StandardError, Exception): + pass + np = NameParser(False, showObj=show_obj, testing=True) + try: + pr = np.parse(r['resource']) + except (StandardError, Exception): + continue + if 0 < Quality.get_proper_level(pr.extra_info_no_name(), pr.version, pr.is_anime): + cl.append(['UPDATE history SET action = ? WHERE rowid = ?', + [Quality.compositeStatus(SNATCHED_PROPER, int(r['quality'])), + r['rowid']]]) + if cl: + my_db.mass_action(cl) + logger.log('Completed the history table update with status Snatched Proper.') + my_db.add_flag('history_snatch_proper') diff --git a/sickbeard/image_cache.py b/sickbeard/image_cache.py index 799d0a66..7d51a7c4 100644 --- a/sickbeard/image_cache.py +++ b/sickbeard/image_cache.py @@ -193,7 +193,7 @@ class ImageCache: img_parser.stream._input.close() msg_success = u'Treating image as %s'\ - + u' with extracted aspect ratio from %s' % path + + u' with extracted aspect ratio from %s' % path.replace('%', '%%') # most posters are around 0.68 width/height ratio (eg. 680/1000) if 0.55 < img_ratio < 0.8: logger.log(msg_success % 'poster', logger.DEBUG) diff --git a/sickbeard/indexermapper.py b/sickbeard/indexermapper.py index b905356a..17c22f04 100644 --- a/sickbeard/indexermapper.py +++ b/sickbeard/indexermapper.py @@ -194,11 +194,26 @@ def get_imdbid_by_name(name, startyear): if hasattr(r, 'movieID') and hasattr(r, 'data') and 'kind' in r.data and r.data['kind'] == 'tv series' \ and 'year' in r.data and r.data['year'] == startyear: ids[INDEXER_IMDB] = tryInt(r.movieID) + break except (StandardError, Exception): pass return {k: v for k, v in ids.iteritems() if v not in (None, '', 0)} +def check_missing_trakt_id(n_ids, show_obj, url_trakt): + if INDEXER_TRAKT not in n_ids: + new_url_trakt = TraktDict() + for k, v in n_ids.iteritems(): + if k != show_obj.indexer and k in [INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_IMDB] and 0 < v \ + and k not in url_trakt: + new_url_trakt[k] = v + + if 0 < len(new_url_trakt): + n_ids.update(get_trakt_ids(new_url_trakt)) + + return n_ids + + def map_indexers_to_show(show_obj, update=False, force=False, recheck=False): """ @@ -281,18 +296,11 @@ def map_indexers_to_show(show_obj, update=False, force=False, recheck=False): or k not in new_ids or new_ids.get(k) in (None, 0, '', MapStatus.NOT_FOUND)} new_ids.update(tvids) - if INDEXER_TRAKT not in new_ids: - new_url_trakt = TraktDict() - for k, v in new_ids.iteritems(): - if k != show_obj.indexer and k in [INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_IMDB] and 0 < v \ - and k not in url_trakt: - new_url_trakt[k] = v - - if 0 < len(new_url_trakt): - new_ids.update(get_trakt_ids(new_url_trakt)) + new_ids = check_missing_trakt_id(new_ids, show_obj, url_trakt) if INDEXER_IMDB not in new_ids: new_ids.update(get_imdbid_by_name(show_obj.name, show_obj.startyear)) + new_ids = check_missing_trakt_id(new_ids, show_obj, url_trakt) if INDEXER_TMDB in mis_map \ and (None is new_ids.get(INDEXER_TMDB) or MapStatus.NOT_FOUND == new_ids.get(INDEXER_TMDB)) \ diff --git a/sickbeard/indexers/indexer_config.py b/sickbeard/indexers/indexer_config.py index 9e7a4cbd..2f508499 100644 --- a/sickbeard/indexers/indexer_config.py +++ b/sickbeard/indexers/indexer_config.py @@ -1,5 +1,5 @@ from lib.tvdb_api.tvdb_api import Tvdb -from lib.tvrage_api.tvrage_api import TVRage +from lib.libtrakt.indexerapiinterface import TraktIndexer INDEXER_TVDB = 1 INDEXER_TVRAGE = 2 @@ -23,7 +23,7 @@ indexerConfig = { id=INDEXER_TVDB, name='TheTVDB', module=Tvdb, - api_params=dict(apikey='F9C450E78D99172E', language='en', useZip=True), + api_params=dict(apikey='F9C450E78D99172E', language='en'), active=True, dupekey='', mapped_only=False, @@ -33,7 +33,7 @@ indexerConfig = { main_url='http://tvrage.com/', id=INDEXER_TVRAGE, name='TVRage', - module=TVRage, + module=None, api_params=dict(apikey='Uhewg1Rr0o62fvZvUIZt', language='en'), active=False, dupekey='tvr', @@ -41,7 +41,7 @@ indexerConfig = { icon='tvrage16.png', ), INDEXER_TVMAZE: dict( - main_url='http://www.tvmaze.com/', + main_url='https://www.tvmaze.com/', id=INDEXER_TVMAZE, name='TVmaze', module=None, @@ -66,9 +66,9 @@ indexerConfig = { main_url='https://www.trakt.tv/', id=INDEXER_TRAKT, name='Trakt', - module=None, + module=TraktIndexer, api_params={}, - active=False, + active=True, dupekey='trakt', mapped_only=True, icon='trakt16.png', @@ -103,13 +103,12 @@ indexerConfig[info_src].update(dict( 'showinfo.php?key=%(apikey)s&sid=' % indexerConfig[info_src]['api_params']), show_url='%sshows/id-' % indexerConfig[info_src]['main_url'], scene_url='https://sickgear.github.io/sg_tvrage_scene_exceptions/exceptions.txt', - xem_origin='rage', defunct=True, )) info_src = INDEXER_TVMAZE indexerConfig[info_src].update(dict( - base_url='http://api.tvmaze.com/', + base_url='https://api.tvmaze.com/', show_url='%sshows/' % indexerConfig[info_src]['main_url'], finder='%ssearch?q=%s' % (indexerConfig[info_src]['main_url'], '%s'), )) diff --git a/sickbeard/indexers/indexer_exceptions.py b/sickbeard/indexers/indexer_exceptions.py index d522235a..173c9422 100644 --- a/sickbeard/indexers/indexer_exceptions.py +++ b/sickbeard/indexers/indexer_exceptions.py @@ -5,28 +5,26 @@ """Custom exceptions used or raised by indexer_api""" -from lib.tvrage_api.tvrage_exceptions import \ - tvrage_exception, tvrage_attributenotfound, tvrage_episodenotfound, tvrage_error, \ - tvrage_seasonnotfound, tvrage_shownotfound, tvrage_userabort - from lib.tvdb_api.tvdb_exceptions import \ tvdb_exception, tvdb_attributenotfound, tvdb_episodenotfound, tvdb_error, \ - tvdb_seasonnotfound, tvdb_shownotfound, tvdb_userabort + tvdb_seasonnotfound, tvdb_shownotfound, tvdb_userabort, tvdb_tokenexpired -indexerExcepts = ["indexer_exception", "indexer_error", "indexer_userabort", "indexer_shownotfound", - "indexer_seasonnotfound", "indexer_episodenotfound", "indexer_attributenotfound"] +indexerExcepts = [ + 'indexer_exception', 'indexer_error', 'indexer_userabort', + 'indexer_shownotfound', 'indexer_seasonnotfound', 'indexer_episodenotfound', + 'indexer_attributenotfound', 'indexer_authenticationerror'] -tvdbExcepts = ["tvdb_exception", "tvdb_error", "tvdb_userabort", "tvdb_shownotfound", - "tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"] - -tvrageExcepts = ["tvdb_exception", "tvrage_error", "tvrage_userabort", "tvrage_shownotfound", - "tvrage_seasonnotfound", "tvrage_episodenotfound", "tvrage_attributenotfound"] +tvdbExcepts = [ + 'tvdb_exception', 'tvdb_error', 'tvdb_userabort', 'tvdb_shownotfound', + 'tvdb_seasonnotfound', 'tvdb_episodenotfound', 'tvdb_attributenotfound', + 'tvdb_tokenexpired'] # link API exceptions to our exception handler -indexer_exception = tvdb_exception, tvrage_exception -indexer_error = tvdb_error, tvrage_error -indexer_userabort = tvdb_userabort, tvrage_userabort -indexer_attributenotfound = tvdb_attributenotfound, tvrage_attributenotfound -indexer_episodenotfound = tvdb_episodenotfound, tvrage_episodenotfound -indexer_seasonnotfound = tvdb_seasonnotfound, tvrage_seasonnotfound -indexer_shownotfound = tvdb_shownotfound, tvrage_shownotfound \ No newline at end of file +indexer_exception = tvdb_exception +indexer_error = tvdb_error +indexer_authenticationerror = tvdb_tokenexpired +indexer_userabort = tvdb_userabort +indexer_attributenotfound = tvdb_attributenotfound +indexer_episodenotfound = tvdb_episodenotfound +indexer_seasonnotfound = tvdb_seasonnotfound +indexer_shownotfound = tvdb_shownotfound \ No newline at end of file diff --git a/sickbeard/metadata/generic.py b/sickbeard/metadata/generic.py index e61bdcec..00d80616 100644 --- a/sickbeard/metadata/generic.py +++ b/sickbeard/metadata/generic.py @@ -517,7 +517,7 @@ class GenericMetadata(): logger.log(u"No thumb is available for this episode, not creating a thumb", logger.DEBUG) return False - thumb_data = metadata_helpers.getShowImage(thumb_url) + thumb_data = metadata_helpers.getShowImage(thumb_url, showName=ep_obj.show.name) result = self._write_image(thumb_data, file_path) @@ -620,7 +620,7 @@ class GenericMetadata(): logger.DEBUG) continue - seasonData = metadata_helpers.getShowImage(season_url) + seasonData = metadata_helpers.getShowImage(season_url, showName=show_obj.name) if not seasonData: logger.log(u"No season poster data available, skipping this season", logger.DEBUG) @@ -668,7 +668,7 @@ class GenericMetadata(): logger.DEBUG) continue - seasonData = metadata_helpers.getShowImage(season_url) + seasonData = metadata_helpers.getShowImage(season_url, showName=show_obj.name) if not seasonData: logger.log(u"No season banner data available, skipping this season", logger.DEBUG) @@ -761,14 +761,19 @@ class GenericMetadata(): # There's gotta be a better way of doing this but we don't wanna # change the language value elsewhere lINDEXER_API_PARMS = sickbeard.indexerApi(show_obj.indexer).api_params.copy() - lINDEXER_API_PARMS['banners'] = True + if image_type.startswith('fanart'): + lINDEXER_API_PARMS['fanart'] = True + elif image_type.startswith('poster'): + lINDEXER_API_PARMS['posters'] = True + else: + lINDEXER_API_PARMS['banners'] = True lINDEXER_API_PARMS['dvdorder'] = 0 != show_obj.dvdorder if indexer_lang and not 'en' == indexer_lang: lINDEXER_API_PARMS['language'] = indexer_lang t = sickbeard.indexerApi(show_obj.indexer).indexer(**lINDEXER_API_PARMS) - indexer_show_obj = t[show_obj.indexerid] + indexer_show_obj = t[show_obj.indexerid, False] except (sickbeard.indexer_error, IOError) as e: logger.log(u"Unable to look up show on " + sickbeard.indexerApi( show_obj.indexer).name + ", not downloading images: " + ex(e), logger.ERROR) @@ -824,7 +829,7 @@ class GenericMetadata(): if return_links: return image_urls else: - image_data = metadata_helpers.getShowImage((init_url, image_urls[0])[None is init_url], which) + image_data = metadata_helpers.getShowImage((init_url, image_urls[0])[None is init_url], which, show_obj.name) if None is not image_data: return image_data diff --git a/sickbeard/metadata/helpers.py b/sickbeard/metadata/helpers.py index 604feebd..70cff329 100644 --- a/sickbeard/metadata/helpers.py +++ b/sickbeard/metadata/helpers.py @@ -20,7 +20,7 @@ from sickbeard import helpers from sickbeard import logger -def getShowImage(url, imgNum=None): +def getShowImage(url, imgNum=None, showName=None): if None is url: return None @@ -32,7 +32,7 @@ def getShowImage(url, imgNum=None): image_data = helpers.getURL(temp_url) if None is image_data: - logger.log(u'There was an error trying to retrieve the image, aborting', logger.ERROR) + logger.log('There was an error trying to retrieve the image%s, aborting' % ('', ' for show: %s' % showName)[None is not showName], logger.ERROR) return return image_data diff --git a/sickbeard/metadata/xbmc_12plus.py b/sickbeard/metadata/xbmc_12plus.py index 8081dfba..ddd74897 100644 --- a/sickbeard/metadata/xbmc_12plus.py +++ b/sickbeard/metadata/xbmc_12plus.py @@ -256,6 +256,9 @@ class XBMC_12PlusMetadata(generic.GenericMetadata): curEpToWrite.episode) + " on " + sickbeard.indexerApi( ep_obj.show.indexer).name + ".. has it been removed? Should I delete from db?") return None + except (StandardError, Exception): + logger.log(u"Not generating nfo because failed to fetched tv info data at this time", logger.DEBUG) + return None if getattr(myEp, 'firstaired', None) is None: myEp["firstaired"] = str(datetime.date.fromordinal(1)) diff --git a/sickbeard/name_cache.py b/sickbeard/name_cache.py index 1a5015af..e98645ca 100644 --- a/sickbeard/name_cache.py +++ b/sickbeard/name_cache.py @@ -18,6 +18,7 @@ import threading import sickbeard from sickbeard import db +from sickbeard.helpers import tryInt nameCache = {} nameCacheLock = threading.Lock() @@ -83,7 +84,7 @@ def buildNameCache(show=None): if cache_results: for cache_result in cache_results: indexer_id = int(cache_result['indexer_id']) - season = int(cache_result['season']) + season = tryInt(cache_result['season'], -1) name = sickbeard.helpers.full_sanitizeSceneName(cache_result['show_name']) nameCache[name] = [indexer_id, season] diff --git a/sickbeard/name_parser/parser.py b/sickbeard/name_parser/parser.py index d4e2e202..62e5b552 100644 --- a/sickbeard/name_parser/parser.py +++ b/sickbeard/name_parser/parser.py @@ -18,14 +18,21 @@ from __future__ import with_statement -import os -import time -import re import datetime +import os import os.path +import re +import time + import regexes import sickbeard +try: + import regex + from math import trunc # positioned here to import only if regex is available +except ImportError: + regex = None + from sickbeard import logger, helpers, scene_numbering, common, scene_exceptions, encodingKludge as ek, db from sickbeard.exceptions import ex @@ -36,7 +43,7 @@ class NameParser(object): ANIME_REGEX = 2 def __init__(self, file_name=True, showObj=None, try_scene_exceptions=False, convert=False, - naming_pattern=False, testing=False): + naming_pattern=False, testing=False, indexer_lookup=True): self.file_name = file_name self.showObj = showObj @@ -44,6 +51,7 @@ class NameParser(object): self.convert = convert self.naming_pattern = naming_pattern self.testing = testing + self.indexer_lookup = indexer_lookup if self.showObj and not self.showObj.is_anime: self._compile_regexes(self.NORMAL_REGEX) @@ -52,6 +60,29 @@ class NameParser(object): else: self._compile_regexes(self.ALL_REGEX) + def _compile_regexes(self, regex_mode): + if self.ANIME_REGEX == regex_mode: + logger.log(u'Using ANIME regexs', logger.DEBUG) + uncompiled_regex = [regexes.anime_regexes] + elif self.NORMAL_REGEX == regex_mode: + logger.log(u'Using NORMAL regexs', logger.DEBUG) + uncompiled_regex = [regexes.normal_regexes] + else: + logger.log(u'Using ALL regexes', logger.DEBUG) + uncompiled_regex = [regexes.normal_regexes, regexes.anime_regexes] + + self.compiled_regexes = {0: [], 1: []} + index = 0 + for regexItem in uncompiled_regex: + for cur_pattern_num, (cur_pattern_name, cur_pattern) in enumerate(regexItem): + try: + cur_regex = re.compile(cur_pattern, re.VERBOSE | re.IGNORECASE) + except re.error as errormsg: + logger.log(u'WARNING: Invalid episode_pattern, %s. %s' % (errormsg, cur_pattern)) + else: + self.compiled_regexes[index].append([cur_pattern_num, cur_pattern_name, cur_regex]) + index += 1 + @staticmethod def clean_series_name(series_name): """Cleans up series name by removing any . and _ @@ -77,43 +108,23 @@ class NameParser(object): series_name = re.sub('^\[.*\]', '', series_name) return series_name.strip() - def _compile_regexes(self, regexMode): - if self.ANIME_REGEX == regexMode: - logger.log(u'Using ANIME regexs', logger.DEBUG) - uncompiled_regex = [regexes.anime_regexes] - elif self.NORMAL_REGEX == regexMode: - logger.log(u'Using NORMAL regexs', logger.DEBUG) - uncompiled_regex = [regexes.normal_regexes] - else: - logger.log(u'Using ALL regexes', logger.DEBUG) - uncompiled_regex = [regexes.normal_regexes, regexes.anime_regexes] - - self.compiled_regexes = {0: [], 1: []} - index = 0 - for regexItem in uncompiled_regex: - for cur_pattern_num, (cur_pattern_name, cur_pattern) in enumerate(regexItem): - try: - cur_regex = re.compile(cur_pattern, re.VERBOSE | re.IGNORECASE) - except re.error as errormsg: - logger.log(u'WARNING: Invalid episode_pattern, %s. %s' % (errormsg, cur_pattern)) - else: - self.compiled_regexes[index].append([cur_pattern_num, cur_pattern_name, cur_regex]) - index += 1 - def _parse_string(self, name): if not name: return matches = [] - for regex in self.compiled_regexes: - for (cur_regex_num, cur_regex_name, cur_regex) in self.compiled_regexes[regex]: + for reg_ex in self.compiled_regexes: + for (cur_regex_num, cur_regex_name, cur_regex) in self.compiled_regexes[reg_ex]: new_name = helpers.remove_non_release_groups(name, 'anime' in cur_regex_name) match = cur_regex.match(new_name) if not match: continue + if 'garbage_name' == cur_regex_name: + return + result = ParseResult(new_name) result.which_regex = [cur_regex_name] result.score = 0 - cur_regex_num @@ -124,6 +135,13 @@ class NameParser(object): result.series_name = match.group('series_name') if result.series_name: result.series_name = self.clean_series_name(result.series_name) + name_parts = re.match('(?i)(.*)[ -]((?:part|pt)[ -]?\w+)$', result.series_name) + try: + result.series_name = name_parts.group(1) + result.extra_info = name_parts.group(2) + except (AttributeError, IndexError): + pass + result.score += 1 if 'series_num' in named_groups and match.group('series_num'): @@ -150,7 +168,7 @@ class NameParser(object): ep = tmp_show.getEpisode(parse_result.season_number, ep_num) else: ep = None - except: + except (StandardError, Exception): ep = None en = ep and ep.name and re.match(r'^\W*(\d+)', ep.name) or None es = en and en.group(1) or None @@ -173,7 +191,13 @@ class NameParser(object): if 'air_year' in named_groups and 'air_month' in named_groups and 'air_day' in named_groups: year = int(match.group('air_year')) - month = int(match.group('air_month')) + try: + month = int(match.group('air_month')) + except ValueError: + try: + month = time.strptime(match.group('air_month')[0:3], '%b').tm_mon + except ValueError as e: + raise InvalidNameException(ex(e)) day = int(match.group('air_day')) # make an attempt to detect YYYY-DD-MM formats if 12 < month: @@ -181,7 +205,8 @@ class NameParser(object): month = day day = tmp_month try: - result.air_date = datetime.date(year, month, day) + result.air_date = datetime.date( + year + ((1900, 2000)[0 < year < 28], 0)[1900 < year], month, day) except ValueError as e: raise InvalidNameException(ex(e)) @@ -192,7 +217,10 @@ class NameParser(object): if tmp_extra_info and 'season_only' == cur_regex_name and re.search( r'([. _-]|^)(special|extra)s?\w*([. _-]|$)', tmp_extra_info, re.I): continue - result.extra_info = tmp_extra_info + if tmp_extra_info: + if result.extra_info: + tmp_extra_info = '%s %s' % (result.extra_info, tmp_extra_info) + result.extra_info = tmp_extra_info result.score += 1 if 'release_group' in named_groups: @@ -233,7 +261,7 @@ class NameParser(object): show = self.showObj best_result.show = show - if show and show.is_anime and 1 < len(self.compiled_regexes[1]) and 1 != regex: + if show and show.is_anime and 1 < len(self.compiled_regexes[1]) and 1 != reg_ex: continue # if this is a naming pattern test then return best result @@ -250,20 +278,35 @@ class NameParser(object): # if we have an air-by-date show then get the real season/episode numbers if best_result.is_air_by_date: + season_number, episode_numbers = None, [] + airdate = best_result.air_date.toordinal() my_db = db.DBConnection() sql_result = my_db.select( - 'SELECT season, episode FROM tv_episodes WHERE showid = ? and indexer = ? and airdate = ?', - [show.indexerid, show.indexer, airdate]) - - season_number = None - episode_numbers = [] + 'SELECT season, episode, name FROM tv_episodes ' + + 'WHERE showid = ? and indexer = ? and airdate = ?', [show.indexerid, show.indexer, airdate]) if sql_result: - season_number = int(sql_result[0][0]) - episode_numbers = [int(sql_result[0][1])] + season_number = int(sql_result[0]['season']) + episode_numbers = [int(sql_result[0]['episode'])] + if 1 < len(sql_result): + # multi-eps broadcast on this day + nums = {'1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five', + '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine', '10': 'ten'} + patt = '(?i)(?:e(?:p(?:isode)?)?|part|pt)[. _-]?(%s)' + try: + src_num = str(re.findall(patt % '\w+', best_result.extra_info)[0]) + alt_num = nums.get(src_num) or list(nums.keys())[list(nums.values()).index(src_num)] + re_partnum = re.compile(patt % ('%s|%s' % (src_num, alt_num))) + for ep_details in sql_result: + if re_partnum.search(ep_details['name']): + season_number = int(ep_details['season']) + episode_numbers = [int(ep_details['episode'])] + break + except (StandardError, Exception): + pass - if not season_number or not len(episode_numbers): + if self.indexer_lookup and not season_number or not len(episode_numbers): try: lindexer_api_parms = sickbeard.indexerApi(show.indexer).api_params.copy() @@ -272,15 +315,17 @@ class NameParser(object): t = sickbeard.indexerApi(show.indexer).indexer(**lindexer_api_parms) - ep_obj = t[show.indexerid].airedOn(best_result.air_date)[0] + ep_obj = t[show.indexerid].aired_on(best_result.air_date)[0] season_number = int(ep_obj['seasonnumber']) episode_numbers = [int(ep_obj['episodenumber'])] except sickbeard.indexer_episodenotfound: - logger.log(u'Unable to find episode with date ' + str(best_result.air_date) + ' for show ' + show.name + ', skipping', logger.WARNING) + logger.log(u'Unable to find episode with date ' + str(best_result.air_date) + + ' for show ' + show.name + ', skipping', logger.WARNING) episode_numbers = [] except sickbeard.indexer_error as e: - logger.log(u'Unable to contact ' + sickbeard.indexerApi(show.indexer).name + ': ' + ex(e), logger.WARNING) + logger.log(u'Unable to contact ' + sickbeard.indexerApi(show.indexer).name + + ': ' + ex(e), logger.WARNING) episode_numbers = [] for epNo in episode_numbers: @@ -410,7 +455,7 @@ class NameParser(object): else: number = 0 - except: + except (StandardError, Exception): # on error try converting from Roman numerals roman_to_int_map = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), @@ -495,7 +540,8 @@ class NameParser(object): % name.encode(sickbeard.SYS_ENCODING, 'xmlcharrefreplace')) # if there's no useful info in it then raise an exception - if None is final_result.season_number and not final_result.episode_numbers and None is final_result.air_date and not final_result.ab_episode_numbers and not final_result.series_name: + if None is final_result.season_number and not final_result.episode_numbers and None is final_result.air_date \ + and not final_result.ab_episode_numbers and not final_result.series_name: raise InvalidNameException('Unable to parse %s' % name.encode(sickbeard.SYS_ENCODING, 'xmlcharrefreplace')) if cache_result: @@ -540,6 +586,7 @@ class ParseResult(object): self.quality = quality self.extra_info = extra_info + self._extra_info_no_name = None self.release_group = release_group self.air_date = air_date @@ -550,6 +597,9 @@ class ParseResult(object): self.version = version + def __ne__(self, other): + return not self.__eq__(other) + def __eq__(self, other): if not other: return False @@ -599,6 +649,37 @@ class ParseResult(object): return to_return.encode('utf-8') + @staticmethod + def _replace_ep_name_helper(e_i_n_n, n): + ep_regex = r'\W*%s\W*' % re.sub(r' ', r'\W', re.sub(r'[^a-zA-Z0-9 ]', r'\W?', + re.sub(r'\W+$', '', n.strip()))) + if None is regex: + return re.sub(ep_regex, '', e_i_n_n, flags=re.I) + + return regex.sub(r'(%s){e<=%d}' % ( + ep_regex, trunc(len(re.findall(r'\w', ep_regex)) / 5)), '', e_i_n_n, flags=regex.I | regex.B) + + def get_extra_info_no_name(self): + extra_info_no_name = self.extra_info + if isinstance(extra_info_no_name, basestring) and self.show and hasattr(self.show, 'indexer'): + for e in self.episode_numbers: + if not hasattr(self.show, 'getEpisode'): + continue + ep = self.show.getEpisode(self.season_number, e) + if ep and isinstance(getattr(ep, 'name', None), basestring) and ep.name.strip(): + extra_info_no_name = self._replace_ep_name_helper(extra_info_no_name, ep.name) + if hasattr(self.show, 'getAllEpisodes'): + for e in [ep.name for ep in self.show.getAllEpisodes(check_related_eps=False) if getattr(ep, 'name', None) + and re.search(r'real|proper|repack', ep.name, re.I)]: + extra_info_no_name = self._replace_ep_name_helper(extra_info_no_name, e) + + return extra_info_no_name + + def extra_info_no_name(self): + if None is self._extra_info_no_name and None is not self.extra_info: + self._extra_info_no_name = self.get_extra_info_no_name() + return self._extra_info_no_name + @property def is_air_by_date(self): if self.air_date: diff --git a/sickbeard/name_parser/regexes.py b/sickbeard/name_parser/regexes.py index bc4fa3b8..d6420e41 100644 --- a/sickbeard/name_parser/regexes.py +++ b/sickbeard/name_parser/regexes.py @@ -19,6 +19,11 @@ # all regexes are case insensitive normal_regexes = [ + ('garbage_name', + ''' + ^[a-zA-Z0-9]{3,}$ + ''' + ), ('standard_repeat', # Show.Name.S01E02.S01E03.Source.Quality.Etc-Group # Show Name - S01E02 - S01E03 - S01E04 - Ep Name @@ -32,7 +37,7 @@ normal_regexes = [ ((?[^- ]+))?)?$ # Group ''' - ), + ), ('fov_repeat', # Show.Name.1x02.1x03.Source.Quality.Etc-Group @@ -47,7 +52,7 @@ normal_regexes = [ ((?[^- ]+))?)?$ # Group ''' - ), + ), ('standard', # Show.Name.S01E02.Source.Quality.Etc-Group @@ -61,12 +66,12 @@ normal_regexes = [ s(?P\d+)[. _-]* # S01 and optional separator e(?P\d+) # E02 and separator (([. _-]*e|-) # linking e/- char - (?P(?!(1080|720|480)[pi])\d+))* # additional E03/etc + (?P(?!(2160|1080|720|480)[pi])\d+))* # additional E03/etc [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^- ]+))?)?$ # Group ''' - ), + ), ('fov', # Show_Name.1x02.Source_Quality_Etc-Group @@ -79,13 +84,13 @@ normal_regexes = [ (?P\d+) # 02 and separator (([. _-]*x|-) # linking x/- char (?P - (?!(1080|720|480)[pi])(?!(?<=x)264) # ignore obviously wrong multi-eps + (?!(2160|1080|720|480)[pi])(?!(?<=x)264) # ignore obviously wrong multi-eps \d+))* # additional x03/etc [\]. _-]*((?P.+?) # Source_Quality_Etc- ((?[^- ]+))?)?$ # Group ''' - ), + ), ('scene_date_format', # Show.Name.2010.11.23.Source.Quality.Etc-Group @@ -99,7 +104,23 @@ normal_regexes = [ ((?[^- ]+))?)?$ # Group ''' - ), + ), + + ('uk_date_format', + # Show.Name.23.11.2010.Source.Quality.Etc-Group + # Show Name - 23-11-2010 - Ep Name + # Show Name - 14-08-17 - Ep Name + # Show Name - 14 Jan 17 - Ep Name + ''' + ^((?P.+?)[. _-]+)? # Show_Name and separator + \(?(?P\d{2})[. _-]+ # 23 and separator + (?P(?:\d{2}|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*))[. _-]+ # 11 and separator + (?P(?:19|20)?\d{2})\)? # 2010 and separator + [. _-]*((?P.+?) # Source_Quality_Etc- + ((?[^- ]+))?)?$ # Group + ''' + ), ('stupid', # tpz-abc102 @@ -109,7 +130,7 @@ normal_regexes = [ (?P\d{1,2}) # 1 (?P\d{2})$ # 02 ''' - ), + ), ('verbose', # Show Name Season 1 Episode 2 Ep Name @@ -121,7 +142,7 @@ normal_regexes = [ (?P\d+)[. _-]+ # 02 and separator (?P.+)$ # Source_Quality_Etc- ''' - ), + ), ('season_only', # Show.Name.S01.Source.Quality.Etc-Group @@ -133,7 +154,7 @@ normal_regexes = [ ((?[^- ]+))?)?$ # Group ''' - ), + ), ('no_season_multi_ep', # Show.Name.E02-03 @@ -143,12 +164,12 @@ normal_regexes = [ (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part (?P(\d+|[ivx]+)) # first ep num ((([. _-]+(and|&|to)[. _-]+)|-) # and/&/to joiner - (?P(?!(1080|720|480)[pi])(\d+|[ivx]+))[. _-]) # second ep num + (?P(?!(2160|1080|720|480)[pi])(\d+|[ivx]+))[. _-]) # second ep num ([. _-]*(?P.+?) # Source_Quality_Etc- ((?[^- ]+))?)?$ # Group ''' - ), + ), ('no_season_general', # Show.Name.E23.Test @@ -160,13 +181,13 @@ normal_regexes = [ (?P(\d+|([ivx]+(?=[. _-])))) # first ep num ([. _-]+((and|&|to)[. _-]+)? # and/&/to joiner ((e(p(isode)?)?|part|pt)[. _-]?) # e, ep, episode, or part - (?P(?!(1080|720|480)[pi]) + (?P(?!(2160|1080|720|480)[pi]) (\d+|([ivx]+(?=[. _-]))))[. _-])* # second ep num ([. _-]*(?P.+?) # Source_Quality_Etc- ((?[^- ]+))?)?$ # Group ''' - ), + ), ('bare', # Show.Name.102.Source.Quality.Etc-Group @@ -177,20 +198,21 @@ normal_regexes = [ ([. _-]+(?P(?!\d{3}[. _-]+)[^-]+) # Source_Quality_Etc- (-(?P.+))?)?$ # Group ''' - ), + ), ('no_season', # Show Name - 01 - Ep Name # 01 - Ep Name ''' ^((?P.+?)(?:[. _-]{2,}|[. _]))? # Show_Name and separator - (?P\d{1,2}) # 01 - (?:-(?P\d{1,2}))* # 02 + (?P\d{1,3}(?!\d)) # 01 + (?:-(?P\d{1,3}(?!\d)))* # 02 + (\s*(?:of)?\s*\d{1,3})? # of num eps [. _-]+((?P.+?) # Source_Quality_Etc- ((?[^- ]+))?)?$ # Group ''' - ), + ), ] anime_regexes = [ @@ -206,7 +228,8 @@ anime_regexes = [ (?:[ ._]?\[(?P\w+)\])? .*? ''' - ), + ), + ('anime_standard', # [Group Name] Show Name.13-14 # [Group Name] Show Name - 13-14 @@ -223,7 +246,9 @@ anime_regexes = [ [ ._-]+\[(?P\d{3,4}[xp]?\d{0,4}.+?)\] # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + ''' + ), + ('anime_standard_round', # [Stratos-Subs]_Infinite_Stratos_-_12_(1280x720_H.264_AAC)_[379759DB] # [ShinBunBu-Subs] Bleach - 02-03 (CX 1280x720 x264 AAC) @@ -236,7 +261,9 @@ anime_regexes = [ [ ._-]+\((?P(CX[ ._-]?)?\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)\) # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + ''' + ), + ('anime_slash', # [SGKK] Bleach 312v1 [720p/MKV] ''' @@ -248,7 +275,9 @@ anime_regexes = [ [ ._-]+\[(?P\d{3,4}p) # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + ''' + ), + ('anime_standard_codec', # [Ayako]_Infinite_Stratos_-_IS_-_07_[H264][720p][EB7838FC] # [Ayako] Infinite Stratos - IS - 07v2 [H264][720p][44419534] @@ -264,7 +293,9 @@ anime_regexes = [ [ ._-]*\[(?P(\d{3,4}[xp]?\d{0,4})?[\.\w\s-]*)\] # Source_Quality_Etc- (\[(?P\w{8})\])? # CRC .*? # Separator and EOL - '''), + ''' + ), + ('anime_and_normal', # Bleach - s16e03-04 - 313-314 # Bleach.s16e03-04.313-314 @@ -283,7 +314,8 @@ anime_regexes = [ (v(?P[0-9]))? # the version e.g. "v2" .*? ''' - ), + ), + ('anime_and_normal_x', # Bleach - s16e03-04 - 313-314 # Bleach.s16e03-04.313-314 @@ -301,7 +333,8 @@ anime_regexes = [ (v(?P[0-9]))? # the version e.g. "v2" .*? ''' - ), + ), + ('anime_and_normal_reverse', # Bleach - 313-314 - s16e03-04 ''' @@ -317,7 +350,8 @@ anime_regexes = [ (?P\d+))* # additional E03/etc .*? ''' - ), + ), + ('anime_and_normal_front', # 165.Naruto Shippuuden.s08e014 ''' @@ -331,7 +365,8 @@ anime_regexes = [ (?P\d+))* # additional E03/etc .*? ''' - ), + ), + ('anime_ep_name', ''' ^(?:\[(?P.+?)\][ ._-]*) @@ -344,7 +379,8 @@ anime_regexes = [ (?:\[(?P\w{8})\])? .*? ''' - ), + ), + ('anime_bare', # One Piece - 102 # [ACX]_Wolf's_Spirit_001.mkv @@ -355,9 +391,10 @@ anime_regexes = [ (-(?P\d{3}))* # E02 (v(?P[0-9]))? # v2 .*? # Separator and EOL - '''), + ''' + ), - ('standard', + ('standard', # Show.Name.S01E02.Source.Quality.Etc-Group # Show Name - S01E02 - My Ep Name # Show.Name.S01.E03.My.Ep.Name @@ -369,10 +406,10 @@ anime_regexes = [ s(?P\d+)[. _-]* # S01 and optional separator e(?P\d+) # E02 and separator (([. _-]*e|-) # linking e/- char - (?P(?!(1080|720|480)[pi])\d+))* # additional E03/etc + (?P(?!(2160|1080|720|480)[pi])\d+))* # additional E03/etc [. _-]*((?P.+?) # Source_Quality_Etc- ((?[^- ]+))?)?$ # Group ''' - ), + ), ] diff --git a/sickbeard/network_timezones.py b/sickbeard/network_timezones.py index 60acd15d..22dc1c79 100644 --- a/sickbeard/network_timezones.py +++ b/sickbeard/network_timezones.py @@ -38,6 +38,9 @@ pm_regex = re.compile(r'(P[. ]? ?M)', flags=re.I) network_dict = None network_dupes = None +last_failure = {'datetime': datetime.datetime.fromordinal(1), 'count': 0} +max_retry_time = 900 +max_retry_count = 3 country_timezones = { 'AU': 'Australia/Sydney', 'AR': 'America/Buenos_Aires', 'AUSTRALIA': 'Australia/Sydney', 'BR': 'America/Sao_Paulo', @@ -49,6 +52,24 @@ country_timezones = { 'TW': 'Asia/Taipei', 'UK': 'Europe/London', 'US': 'US/Eastern', 'ZA': 'Africa/Johannesburg'} +def reset_last_retry(): + global last_failure + last_failure = {'datetime': datetime.datetime.fromordinal(1), 'count': 0} + + +def update_last_retry(): + global last_failure + last_failure = {'datetime': datetime.datetime.now(), 'count': last_failure.get('count', 0) + 1} + + +def should_try_loading(): + global last_failure + if last_failure.get('count', 0) >= max_retry_count and \ + (datetime.datetime.now() - last_failure.get('datetime', datetime.datetime.fromordinal(1))).seconds < max_retry_time: + return False + return True + + def tz_fallback(t): return t if isinstance(t, datetime.tzinfo) else tz.tzlocal() @@ -57,7 +78,7 @@ def get_tz(): t = get_localzone() if isinstance(t, datetime.tzinfo) and hasattr(t, 'zone') and t.zone and hasattr(sickbeard, 'ZONEINFO_DIR'): try: - t = tz_fallback(tz.gettz(t.zone)) + t = tz_fallback(tz.gettz(t.zone, zoneinfo_priority=True)) except: t = tz_fallback(t) else: @@ -99,6 +120,9 @@ def _remove_old_zoneinfo(): # update the dateutil zoneinfo def _update_zoneinfo(): + if not should_try_loading(): + return + global sb_timezone sb_timezone = get_tz() @@ -107,10 +131,13 @@ def _update_zoneinfo(): url_data = helpers.getURL(url_zv) if url_data is None: + update_last_retry() # When urlData is None, trouble connecting to github logger.log(u'Loading zoneinfo.txt failed, this can happen from time to time. Unable to get URL: %s' % url_zv, logger.WARNING) return + else: + reset_last_retry() zonefilename = zoneinfo.ZONEFILENAME cur_zoneinfo = zonefilename @@ -175,6 +202,9 @@ def _update_zoneinfo(): # update the network timezone table def update_network_dict(): + if not should_try_loading(): + return + _remove_old_zoneinfo() _update_zoneinfo() load_network_conversions() @@ -186,10 +216,13 @@ def update_network_dict(): url_data = helpers.getURL(url) if url_data is None: + update_last_retry() # When urlData is None, trouble connecting to github logger.log(u'Updating network timezones failed, this can happen from time to time. URL: %s' % url, logger.WARNING) - load_network_dict() + load_network_dict(load=False) return + else: + reset_last_retry() try: for line in url_data.splitlines(): @@ -231,7 +264,7 @@ def update_network_dict(): # load network timezones from db into dict -def load_network_dict(): +def load_network_dict(load=True): global network_dict, network_dupes my_db = db.DBConnection('cache.db') @@ -240,7 +273,7 @@ def load_network_dict(): sql = 'SELECT %s AS network_name, timezone FROM [network_timezones] ' % sql_name + \ 'GROUP BY %s HAVING COUNT(*) = 1 ORDER BY %s;' % (sql_name, sql_name) cur_network_list = my_db.select(sql) - if cur_network_list is None or len(cur_network_list) < 1: + if load and (cur_network_list is None or len(cur_network_list) < 1): update_network_dict() cur_network_list = my_db.select(sql) network_dict = dict(cur_network_list) @@ -270,14 +303,15 @@ def get_network_timezone(network): if not network_dict: load_network_dict() try: - timezone = tz.gettz(network_dupes.get(network) or network_dict.get(network.replace(' ', '').lower())) + timezone = tz.gettz(network_dupes.get(network) or network_dict.get(network.replace(' ', '').lower()), + zoneinfo_priority=True) except: pass if timezone is None: cc = re.search(r'\(([a-z]+)\)$', network, flags=re.I) try: - timezone = tz.gettz(country_timezones.get(cc.group(1).upper())) + timezone = tz.gettz(country_timezones.get(cc.group(1).upper()), zoneinfo_priority=True) except: pass except: @@ -360,6 +394,9 @@ def standardize_network(network, country): def load_network_conversions(): + if not should_try_loading(): + return + conversions = [] # network conversions are stored on github pages @@ -367,9 +404,12 @@ def load_network_conversions(): url_data = helpers.getURL(url) if url_data is None: + update_last_retry() # When urlData is None, trouble connecting to github logger.log(u'Updating network conversions failed, this can happen from time to time. URL: %s' % url, logger.WARNING) return + else: + reset_last_retry() try: for line in url_data.splitlines(): diff --git a/sickbeard/notifiers/__init__.py b/sickbeard/notifiers/__init__.py index 7faf5685..1ff173c1 100644 --- a/sickbeard/notifiers/__init__.py +++ b/sickbeard/notifiers/__init__.py @@ -18,90 +18,141 @@ import emby import kodi -import xbmc import plex +import xbmc import nmj import nmjv2 import synoindex import synologynotifier import pytivo -import trakt +import boxcar2 +# import pushalot +import pushbullet +import pushover import growl import prowl -from . import libnotify -import pushover -import boxcar2 import nma -import pushalot -import pushbullet +from . import libnotify -import tweet from lib import libtrakt +import trakt +import slack +import discordapp +import gitter +import tweet import emailnotify -# home theater / nas -emby_notifier = emby.EmbyNotifier() -kodi_notifier = kodi.KodiNotifier() -xbmc_notifier = xbmc.XBMCNotifier() -plex_notifier = plex.PLEXNotifier() -nmj_notifier = nmj.NMJNotifier() -nmjv2_notifier = nmjv2.NMJv2Notifier() -synoindex_notifier = synoindex.synoIndexNotifier() -synology_notifier = synologynotifier.synologyNotifier() -pytivo_notifier = pytivo.pyTivoNotifier() -# devices -growl_notifier = growl.GrowlNotifier() -prowl_notifier = prowl.ProwlNotifier() -libnotify_notifier = libnotify.LibnotifyNotifier() -pushover_notifier = pushover.PushoverNotifier() -boxcar2_notifier = boxcar2.Boxcar2Notifier() -nma_notifier = nma.NMA_Notifier() -pushalot_notifier = pushalot.PushalotNotifier() -pushbullet_notifier = pushbullet.PushbulletNotifier() -# social -twitter_notifier = tweet.TwitterNotifier() -trakt_notifier = trakt.TraktNotifier() -email_notifier = emailnotify.EmailNotifier() +import sickbeard -notifiers = [ - libnotify_notifier, # Libnotify notifier goes first because it doesn't involve blocking on network activity. - kodi_notifier, - xbmc_notifier, - plex_notifier, - nmj_notifier, - nmjv2_notifier, - synoindex_notifier, - synology_notifier, - pytivo_notifier, - growl_notifier, - prowl_notifier, - pushover_notifier, - boxcar2_notifier, - nma_notifier, - pushalot_notifier, - pushbullet_notifier, - twitter_notifier, - trakt_notifier, - email_notifier, -] + +class NotifierFactory(object): + + def __init__(self): + self.notifiers = dict( + # home theater / nas + EMBY=emby.EmbyNotifier, + KODI=kodi.KodiNotifier, + PLEX=plex.PLEXNotifier, + # ### XBMC=xbmc.XBMCNotifier, + NMJ=nmj.NMJNotifier, + NMJV2=nmjv2.NMJv2Notifier, + SYNOINDEX=synoindex.SynoIndexNotifier, + SYNOLOGY=synologynotifier.SynologyNotifier, + PYTIVO=pytivo.PyTivoNotifier, + + # devices, + BOXCAR2=boxcar2.Boxcar2Notifier, + # PUSHALOT=pushalot.PushalotNotifier, + PUSHBULLET=pushbullet.PushbulletNotifier, + PUSHOVER=pushover.PushoverNotifier, + GROWL=growl.GrowlNotifier, + PROWL=prowl.ProwlNotifier, + NMA=nma.NMANotifier, + LIBNOTIFY=libnotify.LibnotifyNotifier, + + # social + TRAKT=trakt.TraktNotifier, + SLACK=slack.SlackNotifier, + DISCORDAPP=discordapp.DiscordappNotifier, + GITTER=gitter.GitterNotifier, + TWITTER=tweet.TwitterNotifier, + EMAIL=emailnotify.EmailNotifier, + ) + + @property + def enabled(self): + """ + Generator to yield iterable IDs for enabled notifiers + :return: ID String + :rtype: String + """ + for n in filter(lambda v: v.is_enabled(), self.notifiers.values()): + yield n.id() + + @property + def enabled_onsnatch(self): + for n in filter(lambda v: v.is_enabled() and v.is_enabled_onsnatch(), self.notifiers.values()): + yield n.id() + + @property + def enabled_ondownload(self): + for n in filter(lambda v: v.is_enabled() and v.is_enabled_ondownload(), self.notifiers.values()): + yield n.id() + + @property + def enabled_onsubtitledownload(self): + for n in filter(lambda v: v.is_enabled() and v.is_enabled_onsubtitledownload(), self.notifiers.values()): + yield n.id() + + @property + def enabled_library(self): + for n in filter(lambda v: v.is_enabled() and v.is_enabled_library(), self.notifiers.values()): + yield n.id() + + def get(self, nid): + """ + Get a notifier instance + :param nid: Notified ID + :type nid: String + :return: Notifier instance + :rtype: Notifier + """ + return self.notifiers[nid]() + + def get_enabled(self, kind=None): + """ + Generator to yield iterable notifier instance(s) that are either enabled or enabled for requested actions + :param kind: Action + :type kind: String + :return: Notifier instance + :rtype: Notifier + """ + for n in getattr(self, 'enabled' + ('' if None is kind else ('_' + kind))): + yield self.get(n) + + +def notify_snatch(ep_name): + for n in NotifierFactory().get_enabled('onsnatch'): + n.notify_snatch(ep_name) def notify_download(ep_name): - for n in notifiers: + for n in NotifierFactory().get_enabled('ondownload'): n.notify_download(ep_name) def notify_subtitle_download(ep_name, lang): - for n in notifiers: + for n in NotifierFactory().get_enabled('onsubtitledownload'): n.notify_subtitle_download(ep_name, lang) -def notify_snatch(ep_name): - for n in notifiers: - n.notify_snatch(ep_name) - - def notify_git_update(new_version=''): - for n in notifiers: - n.notify_git_update(new_version) + if sickbeard.NOTIFY_ON_UPDATE: + for n in NotifierFactory().get_enabled(): + n.notify_git_update(new_version) + + +def notify_update_library(ep_obj): + for n in NotifierFactory().get_enabled('library'): + n.update_library(show=ep_obj.show, show_name=ep_obj.show.name, ep_obj=ep_obj) diff --git a/sickbeard/notifiers/boxcar2.py b/sickbeard/notifiers/boxcar2.py index 72140eaa..864875b6 100755 --- a/sickbeard/notifiers/boxcar2.py +++ b/sickbeard/notifiers/boxcar2.py @@ -18,115 +18,79 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import time import urllib import urllib2 -import time import sickbeard - -from sickbeard import logger -from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT from sickbeard.exceptions import ex - -API_URL = 'https://new.boxcar.io/api/notifications' +from sickbeard.notifiers.generic import Notifier -class Boxcar2Notifier: - def _sendBoxcar2(self, title, msg, accesstoken, sound): +class Boxcar2Notifier(Notifier): + + def __init__(self): + super(Boxcar2Notifier, self).__init__() + + self.sg_logo_file = 'apple-touch-icon-60x60.png' + + def _notify(self, title, body, access_token=None, sound=None, **kwargs): """ Sends a boxcar2 notification to the address provided - - msg: The message to send + title: The title of the message - accesstoken: to send to this device + body: The message to send + access_token: To send to this device + sound: Sound profile to use returns: True if the message succeeded, False otherwise """ + access_token = self._choose(access_token, sickbeard.BOXCAR2_ACCESSTOKEN) + sound = self._choose(sound, sickbeard.BOXCAR2_SOUND) # build up the URL and parameters - # more info goes here - https://boxcar.uservoice.com/knowledgebase/articles/306788-how-to-send-your-boxcar-account-a-notification - msg = msg.strip().encode('utf-8') + # more info goes here - + # https://boxcar.uservoice.com/knowledgebase/articles/306788-how-to-send-your-boxcar-account-a-notification + body = body.strip().encode('utf-8') data = urllib.urlencode({ - 'user_credentials': accesstoken, - 'notification[title]': title + ' - ' + msg, - 'notification[long_message]': msg, + 'user_credentials': access_token, + 'notification[title]': '%s - %s' % (title, body), + 'notification[long_message]': body, 'notification[sound]': sound, 'notification[source_name]': 'SickGear', - 'notification[icon_url]': 'https://cdn.rawgit.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon-60x60.png' + 'notification[icon_url]': self._sg_logo_url }) # send the request to boxcar2 + result = None try: - req = urllib2.Request(API_URL) + req = urllib2.Request('https://new.boxcar.io/api/notifications') handle = urllib2.urlopen(req, data) handle.close() except urllib2.URLError as e: - # if we get an error back that doesn't have an error code then who knows what's really happening if not hasattr(e, 'code'): - logger.log(u'BOXCAR2: Notification failed.' + ex(e), logger.ERROR) + self._log_error(u'Notification failed: %s' % ex(e)) else: - logger.log(u'BOXCAR2: Notification failed. Error code: ' + str(e.code), logger.ERROR) + result = 'Notification failed. Error code: %s' % e.code + self._log_error(result) - if e.code == 404: - logger.log(u'BOXCAR2: Access token is wrong/not associated to a device.', logger.ERROR) - elif e.code == 401: - logger.log(u'BOXCAR2: Access token not recognized.', logger.ERROR) - elif e.code == 400: - logger.log(u'BOXCAR2: Wrong data sent to boxcar.', logger.ERROR) - elif e.code == 503: - logger.log(u'BOXCAR2: Boxcar server to busy to handle the request at this time.', logger.WARNING) - return False + if 503 == e.code: + result = 'Server too busy to handle the request at this time' + self._log_warning(result) + else: + if 404 == e.code: + result = 'Access token is wrong/not associated to a device' + self._log_error(result) + elif 401 == e.code: + result = 'Access token not recognized' + self._log_error(result) + elif 400 == e.code: + result = 'Wrong data sent to Boxcar' + self._log_error(result) - logger.log(u'BOXCAR2: Notification successful.', logger.MESSAGE) - return True + return self._choose((True, 'Failed to send notification: %s' % result)[bool(result)], not bool(result)) - def _notifyBoxcar2(self, title, message, accesstoken=None, sound=None, force=False): - """ - Sends a boxcar2 notification based on the provided info or SG config - - title: The title of the notification to send - message: The message string to send - accesstoken: to send to this device - force: If True then the notification will be sent even if Boxcar is disabled in the config - """ - - # suppress notifications if the notifier is disabled but the notify options are checked - if not sickbeard.USE_BOXCAR2 and not force: - logger.log(u'BOXCAR2: Notifications are not enabled, skipping this notification', logger.DEBUG) - return False - - # fill in omitted parameters - if not accesstoken: - accesstoken = sickbeard.BOXCAR2_ACCESSTOKEN - if not sound: - sound = sickbeard.BOXCAR2_SOUND - - logger.log(u'BOXCAR2: Sending notification for ' + message, logger.DEBUG) - - self._sendBoxcar2(title, message, accesstoken, sound) - return True - - def test_notify(self, accesstoken, sound, force=True): - return self._sendBoxcar2('Test', 'This is a test notification from SickGear', accesstoken, sound) - - def notify_snatch(self, ep_name): - if sickbeard.BOXCAR2_NOTIFY_ONSNATCH: - self._notifyBoxcar2(notifyStrings[NOTIFY_SNATCH], ep_name) - - def notify_download(self, ep_name): - if sickbeard.BOXCAR2_NOTIFY_ONDOWNLOAD: - self._notifyBoxcar2(notifyStrings[NOTIFY_DOWNLOAD], ep_name) - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notifyBoxcar2(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_BOXCAR2: - update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT] - title=notifyStrings[NOTIFY_GIT_UPDATE] - self._notifyBoxcar2(title, update_text + new_version) notifier = Boxcar2Notifier diff --git a/sickbeard/notifiers/discordapp.py b/sickbeard/notifiers/discordapp.py new file mode 100644 index 00000000..c0f34803 --- /dev/null +++ b/sickbeard/notifiers/discordapp.py @@ -0,0 +1,47 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# Thanks to: mallen86, generica +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import sickbeard +from sickbeard.notifiers.generic import Notifier + + +class DiscordappNotifier(Notifier): + + def __init__(self): + super(DiscordappNotifier, self).__init__() + + def _notify(self, title, body, as_authed=None, username='', icon_url='', as_tts='', access_token='', **kwargs): + params = [] if not bool(self._choose(not as_authed, sickbeard.DISCORDAPP_AS_AUTHED)) else \ + [('username', self._choose(username, sickbeard.DISCORDAPP_USERNAME) or 'SickGear'), + ('avatar_url', self._choose(icon_url, sickbeard.DISCORDAPP_ICON_URL) or self._sg_logo_url)] + as_tts = self._choose(as_tts, bool(sickbeard.DISCORDAPP_AS_TTS)) + + resp = sickbeard.helpers.getURL( + url=self._choose(access_token, sickbeard.DISCORDAPP_ACCESS_TOKEN), + post_json=dict([('content', self._body_only(title, body)), ('tts', as_tts)] + params)) + + result = '' == resp or self._choose('bad webhook?', None) + if True is not result: + self._log_error('%s failed to send message: %s' % (self.name, result)) + + return self._choose(('Success, notification sent. (Note: %s clients display icon once in a sequence)' + % self.name, 'Failed to send notification, %s' % result)[True is not result], result) + + +notifier = DiscordappNotifier diff --git a/sickbeard/notifiers/emailnotify.py b/sickbeard/notifiers/emailnotify.py index 49692559..a5978585 100644 --- a/sickbeard/notifiers/emailnotify.py +++ b/sickbeard/notifiers/emailnotify.py @@ -19,118 +19,57 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import re - -import sickbeard -import smtplib - -from sickbeard import db -from sickbeard import logger -from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD - from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formatdate +import re +import smtplib + +import sickbeard +from sickbeard import db +from sickbeard.notifiers.generic import Notifier, notify_strings -class EmailNotifier: +class EmailNotifier(Notifier): def __init__(self): + super(EmailNotifier, self).__init__() self.last_err = None - def test_notify(self, host, port, smtp_from, use_tls, user, pwd, to): + def _sendmail(self, host, port, smtp_from, use_tls, user, pwd, to, msg, smtp_debug=False): - msg = MIMEText('Success. This is a SickGear test message. Typically sent on, %s' % - notifyStrings[NOTIFY_DOWNLOAD]) - msg['Subject'] = 'SickGear: Test message' - msg['From'] = smtp_from - msg['To'] = to - msg['Date'] = formatdate(localtime=True) - return self._sendmail(host, port, smtp_from, use_tls, user, pwd, [to], msg, True) - - def _send_email(self, title, ep_name, lang='', extra='', force=False): - - if not sickbeard.USE_EMAIL and not force: - return - - show = ep_name.split(' - ')[0] - to = self._get_recipients(show) - if not any(to): - logger.log(u'No email recipients to notify, skipping', logger.WARNING) - return - - logger.log(u'Email recipients to notify: %s' % to, logger.DEBUG) + use_tls = 1 == sickbeard.helpers.tryInt(use_tls) + login = any(user) and any(pwd) + self._log_debug(u'Sendmail HOST: %s; PORT: %s; LOGIN: %s, TLS: %s, USER: %s, FROM: %s, TO: %s' % ( + host, port, login, use_tls, user, smtp_from, to)) try: - msg = MIMEMultipart('alternative') - msg.attach(MIMEText( - '' + - '

SickGear Notification - %s

\n' % title + - '

Show: ' + show.encode('ascii', 'xmlcharrefreplace') + - '

\n

Episode: ' + - unicode(re.search('.+ - (.+?-.+) -.+', ep_name).group(1)).encode('ascii', 'xmlcharrefreplace') + - extra + - '

\n\n' + - '
' + - 'Powered by SickGear.
', - 'html')) - except: - try: - msg = MIMEText(ep_name) - except: - msg = MIMEText('Episode %s' % title) + srv = smtplib.SMTP(host, int(port)) + if smtp_debug: + srv.set_debuglevel(1) - msg['Subject'] = '%s%s: %s' % (lang, title, ep_name) - msg['From'] = sickbeard.EMAIL_FROM - msg['To'] = ','.join(to) - msg['Date'] = formatdate(localtime=True) - if self._sendmail(sickbeard.EMAIL_HOST, sickbeard.EMAIL_PORT, sickbeard.EMAIL_FROM, sickbeard.EMAIL_TLS, - sickbeard.EMAIL_USER, sickbeard.EMAIL_PASSWORD, to, msg): - logger.log(u'%s notification sent to [%s] for "%s"' % (title, to, ep_name), logger.DEBUG) - else: - logger.log(u'%s notification ERROR: %s' % (title, self.last_err), logger.ERROR) + if use_tls or login: + srv.ehlo() + self._log_debug(u'Sent initial EHLO command') - def notify_snatch(self, ep_name, title=notifyStrings[NOTIFY_SNATCH]): - """ - Send a notification that an episode was snatched + if use_tls: + srv.starttls() + srv.ehlo() + self._log_debug(u'Sent STARTTLS and EHLO command') - :param ep_name: The name of the episode that was snatched - :param title: The title of the notification (optional) - """ + if login: + srv.login(user, pwd) + self._log_debug(u'Sent LOGIN command') - if sickbeard.EMAIL_NOTIFY_ONSNATCH: - title = sickbeard.EMAIL_OLD_SUBJECTS and 'Snatched' or title - self._send_email(title, ep_name) + srv.sendmail(smtp_from, to, msg.as_string()) + srv.quit() - def notify_download(self, ep_name, title=notifyStrings[NOTIFY_DOWNLOAD]): - """ - Send a notification that an episode was downloaded + except Exception as e: + self.last_err = '%s' % e + return False - :param ep_name: The name of the episode that was downloaded - :param title: The title of the notification (optional) - """ - - if sickbeard.EMAIL_NOTIFY_ONDOWNLOAD: - title = sickbeard.EMAIL_OLD_SUBJECTS and 'Downloaded' or title - self._send_email(title, ep_name) - - def notify_subtitle_download(self, ep_name, lang, title=notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD]): - """ - Send a notification that a subtitle was downloaded - - :param ep_name: The name of the episode that was downloaded - :param lang: Subtitle language - :param title: The title of the notification (optional) - """ - - if sickbeard.EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD: - title = sickbeard.EMAIL_OLD_SUBJECTS and 'Subtitle Downloaded' or title - self._send_email(title, ep_name, '%s ' % lang, '

\n

Language: %s' % lang) - - def notify_git_update(self, new_version='??'): - - pass + return True @staticmethod def _get_recipients(show_name=None): @@ -154,39 +93,91 @@ class EmailNotifier: return list(set(email_list)) - def _sendmail(self, host, port, smtp_from, use_tls, user, pwd, to, msg, smtp_debug=False): + def _notify(self, title, body, lang='', extra='', **kwargs): - use_tls = 1 == sickbeard.helpers.tryInt(use_tls) - login = any(user) and any(pwd) - logger.log(u'Sendmail HOST: %s; PORT: %s; LOGIN: %s, TLS: %s, USER: %s, FROM: %s, TO: %s' % ( - host, port, login, use_tls, user, smtp_from, to), logger.DEBUG) + show = body.split(' - ')[0] + to = self._get_recipients(show) + if not any(to): + self._log_warning(u'No email recipients to notify, skipping') + return + + self._log_debug(u'Email recipients to notify: %s' % to) try: - srv = smtplib.SMTP(host, int(port)) - if smtp_debug: - srv.set_debuglevel(1) + msg = MIMEMultipart('alternative') + msg.attach(MIMEText( + '' + + '

SickGear Notification - %s

\n' % title + + '

Show: ' + show.encode('ascii', 'xmlcharrefreplace') + + '

\n

Episode: ' + + unicode(re.search('.+ - (.+?-.+) -.+', body).group(1)).encode('ascii', 'xmlcharrefreplace') + + extra + + '

\n\n' + + '
' + + 'Powered by SickGear.
', + 'html')) + except (StandardError, Exception): + try: + msg = MIMEText(body) + except (StandardError, Exception): + msg = MIMEText('Episode %s' % title) - if use_tls or login: - srv.ehlo() - logger.log(u'Sent initial EHLO command', logger.DEBUG) + msg['Subject'] = '%s%s: %s' % (lang, title, body) + msg['From'] = sickbeard.EMAIL_FROM + msg['To'] = ','.join(to) + msg['Date'] = formatdate(localtime=True) + if self._sendmail(sickbeard.EMAIL_HOST, sickbeard.EMAIL_PORT, sickbeard.EMAIL_FROM, sickbeard.EMAIL_TLS, + sickbeard.EMAIL_USER, sickbeard.EMAIL_PASSWORD, to, msg): + self._log_debug(u'%s notification sent to [%s] for "%s"' % (title, to, body)) + else: + self._log_error(u'%s notification ERROR: %s' % (title, self.last_err)) - if use_tls: - srv.starttls() - srv.ehlo() - logger.log(u'Sent STARTTLS and EHLO command', logger.DEBUG) + def test_notify(self, host, port, smtp_from, use_tls, user, pwd, to): + self._testing = True - if login: - srv.login(user, pwd) - logger.log(u'Sent LOGIN command', logger.DEBUG) + msg = MIMEText('Success. This is a SickGear test message. Typically sent on, %s' % notify_strings['download']) + msg['Subject'] = 'SickGear: Test message' + msg['From'] = smtp_from + msg['To'] = to + msg['Date'] = formatdate(localtime=True) - srv.sendmail(smtp_from, to, msg.as_string()) - srv.quit() + r = self._sendmail(host, port, smtp_from, use_tls, user, pwd, [to], msg, True) + return self._choose(('Success, notification sent.', + 'Failed to send notification: %s' % self.last_err)[not r], r) - except Exception as e: - self.last_err = '%s' % e - return False + def notify_snatch(self, ep_name, title=None): + """ + Send a notification that an episode was snatched - return True + :param ep_name: The name of the episode that was snatched + :param title: The title of the notification (optional) + """ + + title = sickbeard.EMAIL_OLD_SUBJECTS and 'Snatched' or title or notify_strings['snatch'] + self._notify(title, ep_name) + + def notify_download(self, ep_name, title=None): + """ + Send a notification that an episode was downloaded + + :param ep_name: The name of the episode that was downloaded + :param title: The title of the notification (optional) + """ + + title = sickbeard.EMAIL_OLD_SUBJECTS and 'Downloaded' or title or notify_strings['download'] + self._notify(title, ep_name) + + def notify_subtitle_download(self, ep_name, lang, title=None): + """ + Send a notification that a subtitle was downloaded + + :param ep_name: The name of the episode that was downloaded + :param lang: Subtitle language + :param title: The title of the notification (optional) + """ + + title = sickbeard.EMAIL_OLD_SUBJECTS and 'Subtitle Downloaded' or title or notify_strings['subtitle_download'] + self._notify(title, ep_name, '%s ' % lang, '

\n

Language: %s' % lang) notifier = EmailNotifier diff --git a/sickbeard/notifiers/emby.py b/sickbeard/notifiers/emby.py index 71e279f5..62fb322c 100644 --- a/sickbeard/notifiers/emby.py +++ b/sickbeard/notifiers/emby.py @@ -15,68 +15,31 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import sickbeard -from sickbeard import logger from socket import socket, AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR, SO_BROADCAST, SHUT_RDWR + +import sickbeard +from sickbeard.notifiers.generic import Notifier + from lib import simplejson as json -class EmbyNotifier: +class EmbyNotifier(Notifier): + def __init__(self): - self.sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/' + \ - 'apple-touch-icon-precomposed.png' + super(EmbyNotifier, self).__init__() + self.response = None - self.test_mode = False - def _notify_emby(self, msg, hosts=None, apikeys=None): - """ Internal wrapper for the test_notify function + def update_library(self, show=None, **kwargs): + """ Update library function - Args: - msg: Message body of the notice to send + :param show: TVShow object - Returns: - 2-Tuple True if msg successfully sent otherwise False, Failure message string or None + Returns: None if no processing done, True if processing succeeded with no issues else False if any issues found """ - - if not sickbeard.USE_EMBY and not self.test_mode: - self._log(u'Notification not enabled, skipping this notification', logger.DEBUG) - return False, None - - hosts, keys, message = self._check_config(hosts, apikeys) - if not hosts: - return False, message - - total_success = True - messages = [] - - args = dict(post_json={'Name': 'SickGear', 'Description': msg, 'ImageUrl': self.sg_logo_url}) - for i, cur_host in enumerate(hosts): - - self.response = None - response = sickbeard.helpers.getURL( - 'http://%s/emby/Notifications/Admin' % cur_host, - headers={'Content-type': 'application/json', 'X-MediaBrowser-Token': keys[i]}, - timeout=10, hooks=dict(response=self._cb_response), **args) - if not response or self.response: - if self.response and 401 == self.response.get('status_code'): - total_success = False - messages += ['Fail: Cannot authenticate API key with %s' % cur_host] - self._log(u'Failed to authenticate with %s' % cur_host) - continue - elif not response and not self.response or not self.response.get('ok'): - total_success = False - messages += ['Fail: No supported Emby server found at %s' % cur_host] - self._log(u'Warning, could not connect with server at ' + cur_host) - continue - messages += ['OK: %s' % cur_host] - - return total_success, '
\n'.join(messages) - - def _update_library(self, show=None): - hosts, keys, message = self._check_config() if not hosts: - self._log(u'Issue with hosts or api keys, check your settings') + self._log_warning(u'Issue with hosts or api keys, check your settings') return False from sickbeard.indexers.indexer_config import INDEXER_TVDB @@ -94,30 +57,28 @@ class EmbyNotifier: timeout=20, hooks=dict(response=self._cb_response), **args) # Emby will initiate a LibraryMonitor path refresh one minute after this success if self.response and 204 == self.response.get('status_code') and self.response.get('ok'): - self._log(u'Success: update %s sent to host %s in a library updated call' % (mode_to_log, cur_host), - logger.MESSAGE) + self._log(u'Success: update %s sent to host %s in a library updated call' % (mode_to_log, cur_host)) continue elif self.response and 401 == self.response.get('status_code'): - self._log(u'Failed to authenticate with %s' % cur_host) + self._log_warning(u'Failed to authenticate with %s' % cur_host) elif self.response and 404 == self.response.get('status_code'): - self._log(u'Warning, Library update responded 404 not found at %s' % cur_host, logger.DEBUG) + self._log_debug(u'Warning, Library update responded 404 not found at %s' % cur_host) elif not response and not self.response or not self.response.get('ok'): - self._log(u'Warning, could not connect with server at %s' % cur_host) + self._log_warning(u'Warning, could not connect with server at %s' % cur_host) else: - self._log(u'Warning, unknown response %sfrom %s, can most likely be ignored' - % (self.response and '%s ' % self.response.get('status_code') or '', cur_host), logger.DEBUG) + self._log_debug(u'Warning, unknown response %sfrom %s, can most likely be ignored' + % (self.response and '%s ' % self.response.get('status_code') or '', cur_host)) total_success = False return total_success # noinspection PyUnusedLocal def _cb_response(self, r, *args, **kwargs): - self.response = dict(status_code=r.status_code, ok=r.ok) return r - @staticmethod - def _discover_server(): + def _discover_server(self): + cs = socket(AF_INET, SOCK_DGRAM) mb_listen_port = 7359 @@ -132,14 +93,14 @@ class EmbyNotifier: 'Not all data sent through the socket' message, host = cs.recvfrom(1024) if message: - logger.log('%s found at %s: udp query response (%s)' % (server, host[0], message)) + self._log('%s found at %s: udp query response (%s)' % (server, host[0], message)) result = ('{"Address":' not in message and message.split('|')[1] or json.loads(message).get('Address', '')) if result: break except AssertionError: sock_issue = True - except Exception: + except (StandardError, Exception): pass if not sock_issue: cs.shutdown(SHUT_RDWR) @@ -149,7 +110,7 @@ class EmbyNotifier: from sickbeard.helpers import starify - hosts, keys = hosts or sickbeard.EMBY_HOST, apikeys or sickbeard.EMBY_APIKEY + hosts, keys = self._choose(hosts, sickbeard.EMBY_HOST), self._choose(apikeys, sickbeard.EMBY_APIKEY) hosts = [x.strip() for x in hosts.split(',') if x.strip()] keys = [x.strip() for x in keys.split(',') if x.strip()] @@ -165,15 +126,50 @@ class EmbyNotifier: if len(hosts) != len(apikeys): message = ('Not enough Api keys for hosts', 'More Api keys than hosts')[len(apikeys) > len(hosts)] - self._log(u'%s, check your settings' % message) + self._log_warning(u'%s, check your settings' % message) return False, False, message return hosts, apikeys, 'OK' - @staticmethod - def _log(msg, log_level=logger.WARNING): + def _notify(self, title, body, hosts, apikeys, **kwargs): + """ Internal wrapper for the test_notify function - logger.log(u'Emby: %s' % msg, log_level) + Args: + title: The title of the message + body: Message body of the notice to send + + Returns: + 2-Tuple True if body successfully sent otherwise False, Failure message string or None + """ + hosts, keys, message = self._check_config(hosts, apikeys) + if not hosts: + return False, message + + success = True + message = [] + + args = dict(post_json={'Name': 'SickGear', 'Description': body, 'ImageUrl': self._sg_logo_url}) + for i, cur_host in enumerate(hosts): + + self.response = None + response = sickbeard.helpers.getURL( + 'http://%s/emby/Notifications/Admin' % cur_host, + headers={'Content-type': 'application/json', 'X-MediaBrowser-Token': keys[i]}, + timeout=10, hooks=dict(response=self._cb_response), **args) + if not response or self.response: + if self.response and 401 == self.response.get('status_code'): + success = False + message += ['Fail: Cannot authenticate API key with %s' % cur_host] + self._log_warning(u'Failed to authenticate with %s' % cur_host) + continue + elif not response and not self.response or not self.response.get('ok'): + success = False + message += ['Fail: No supported Emby server found at %s' % cur_host] + self._log_warning(u'Warning, could not connect with server at ' + cur_host) + continue + message += ['OK: %s' % cur_host] + + return self._choose(('Success, all hosts tested', '
\n'.join(message))[not success], success) ############################################################################## # Public functions @@ -182,23 +178,5 @@ class EmbyNotifier: def discover_server(self): return self._discover_server() - def test_notify(self, host, apikey): - - self.test_mode = True - result = self._notify_emby('Testing SickGear Emby notifier', host, apikey) - self.test_mode = False - return result - - def update_library(self, show=None, force=False): - """ Wrapper for the update library functions - - :param show: TVShow object - :param force: True force update process - - Returns: None if no processing done, True if processing succeeded with no issues else False if any issues found - """ - if sickbeard.USE_EMBY and (sickbeard.EMBY_UPDATE_LIBRARY or force): - return self._update_library(show) - notifier = EmbyNotifier diff --git a/sickbeard/notifiers/generic.py b/sickbeard/notifiers/generic.py new file mode 100644 index 00000000..2fc2358c --- /dev/null +++ b/sickbeard/notifiers/generic.py @@ -0,0 +1,141 @@ +# coding=utf-8 +# +# Author: Nic Wolfe +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import sickbeard +from sickbeard import logger + +notify_strings = dict( + snatch='Started download', + download='Download finished', + subtitle_download='Subtitle download finished', + git_updated='SickGear updated', + git_updated_text='SickGear updated to commit#: ', + test_title='SickGear notification test', + test_body=u'Success testing %s settings from SickGear ʕ•ᴥ•ʔ', +) + + +class BaseNotifier(object): + + def __init__(self): + self.sg_logo_file = 'apple-touch-icon-precomposed.png' + self._testing = False + + @property + def _sg_logo_url(self): + return 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/' + self.sg_logo_file + + def _log(self, msg, level=logger.MESSAGE): + logger.log(u'%s: %s' % (self.name, msg), level) + + def _log_debug(self, msg): + self._log(msg, logger.DEBUG) + + def _log_error(self, msg): + self._log(msg, logger.ERROR) + + def _log_warning(self, msg): + self._log(msg, logger.WARNING) + + @classmethod + def id(cls): + return cls.__name__.replace('Notifier', '').upper() + + @property + def name(self): + return self.__class__.__name__.replace('Notifier', '') + + @classmethod + def is_enabled_onsnatch(cls): + return cls.is_enabled('NOTIFY_ONSNATCH') + + @classmethod + def is_enabled_ondownload(cls): + return cls.is_enabled('NOTIFY_ONDOWNLOAD') + + @classmethod + def is_enabled_onsubtitledownload(cls): + return cls.is_enabled('NOTIFY_ONSUBTITLEDOWNLOAD') + + @classmethod + def is_enabled_library(cls): + return cls.is_enabled('UPDATE_LIBRARY') + + @classmethod + def is_enabled(cls, action=None): + return getattr(sickbeard, action and '%s_%s' % (cls.id(), action) or 'USE_%s' % cls.id(), False) + + def notify_snatch(self, *args, **kwargs): + pass + + def notify_download(self, *args, **kwargs): + pass + + def notify_subtitle_download(self, *args, **kwargs): + pass + + def notify_git_update(self, *args, **kwargs): + pass + + def update_library(self, **kwargs): + """ + note: nmj_notifier fires its library update when the notify_download is issued (inside notifiers) + """ + pass + + def _notify(self, *args, **kwargs): + pass + + def _choose(self, current=True, saved=True): + if self._testing: + return current + return saved + + @staticmethod + def _body_only(title, body): + # don't use title with updates or testing, as only one str is used + return body if 'SickGear' in title else '%s: %s' % (title, body.replace('#: ', '# ')) + + +class Notifier(BaseNotifier): + + def test_notify(self, *args, **kwargs): + self._testing = True + r = self._pre_notify('test_title', notify_strings['test_body'] % (self.name + ' notifier'), *args, **kwargs) + return (r, (('Success, notification sent.', 'Failed to send notification.')[not r]))[r in (True, False)] + + def notify_snatch(self, ep_name, **kwargs): + self._pre_notify('snatch', ep_name, **kwargs) + + def notify_download(self, ep_name, **kwargs): + self._pre_notify('download', ep_name, **kwargs) + + def notify_subtitle_download(self, ep_name, lang, **kwargs): + self._pre_notify('subtitle_download', '%s : %s' % (ep_name, lang), **kwargs) + + def notify_git_update(self, new_version='??', **kwargs): + self._pre_notify('git_updated', notify_strings['git_updated_text'] + new_version, **kwargs) + + def _pre_notify(self, notify_string, message, *args, **kwargs): + self._log_debug(u'Sending notification "%s"' % (self._body_only(notify_strings[notify_string], message))) + try: + return self._notify(notify_strings[notify_string], message, *args, **kwargs) + except (StandardError, Exception): + return False diff --git a/sickbeard/notifiers/gitter.py b/sickbeard/notifiers/gitter.py new file mode 100644 index 00000000..49022fbf --- /dev/null +++ b/sickbeard/notifiers/gitter.py @@ -0,0 +1,80 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# Thanks to: mallen86, generica +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import sickbeard +from sickbeard.notifiers.generic import Notifier + + +class GitterNotifier(Notifier): + + def __init__(self): + super(GitterNotifier, self).__init__() + + def _notify(self, title, body, room_name='', access_token='', **kwargs): + + api_url = 'https://api.gitter.im/v1/' + params = [('headers', dict( + Authorization='Bearer %s' % self._choose(access_token, sickbeard.GITTER_ACCESS_TOKEN))), ('json', True)] + is_locked = False + + # get user of token + # noinspection PyTypeChecker + resp = sickbeard.helpers.getURL(**dict([('url', '%suser' % api_url)] + params)) + user_id = resp and 1 == len(resp) and resp[0].get('id') or None + if None is user_id: + result = self._failed('bad oath access token?') + else: + # get a room + # noinspection PyTypeChecker + resp = sickbeard.helpers.getURL(**dict( + [('url', '%srooms' % api_url), + ('post_json', dict(uri=self._choose(room_name, sickbeard.GITTER_ROOM)))] + params)) + room_id = resp and resp.get('id') or None + if None is room_id: + result = self._failed('room locked or not found') + else: + is_locked = 'private' == resp.get('security', '').lower() + + # join room + # noinspection PyTypeChecker + if not sickbeard.helpers.getURL(**dict( + [('url', '%suser/%s/rooms' % (api_url, user_id)), + ('post_json', dict(id=room_id))] + params)): + result = self._failed('failed to join room') + else: + # send text + # noinspection PyTypeChecker + resp = sickbeard.helpers.getURL(**dict( + [('url', '%srooms/%s/chatMessages' % (api_url, room_id)), + ('post_json', dict(text=self._body_only(title, body)))] + params)) + if None is (resp and resp.get('id') or None): + result = self._failed('failed to send text', append=False) + else: + result = True + + return self._choose(('Error sending notification, %s' % result, + 'Successful test notice sent%s. (Note: %s clients display icon once in a sequence).' % + (('', ' to locked room')[is_locked], self.name))[True is result], result) + + def _failed(self, result, append=True): + self._log_error('%s failed to send message%s' % (self.name, append and ', %s' % result or '')) + return self._choose(result, None) + + +notifier = GitterNotifier diff --git a/sickbeard/notifiers/growl.py b/sickbeard/notifiers/growl.py index 370e0742..56bc68a5 100644 --- a/sickbeard/notifiers/growl.py +++ b/sickbeard/notifiers/growl.py @@ -18,45 +18,28 @@ from __future__ import print_function import socket +import urllib import sickbeard - -from sickbeard import logger, common from sickbeard.exceptions import ex +from sickbeard.notifiers.generic import Notifier, notify_strings from lib.growl import gntp -class GrowlNotifier: - def test_notify(self, host, password): - self._sendRegistration(host, password, 'Test') - return self._sendGrowl('Test Growl', 'Testing Growl settings from SickGear', 'Test', host, password, - force=True) +class GrowlNotifier(Notifier): - def notify_snatch(self, ep_name): - if sickbeard.GROWL_NOTIFY_ONSNATCH: - self._sendGrowl(common.notifyStrings[common.NOTIFY_SNATCH], ep_name) + def __init__(self): + super(GrowlNotifier, self).__init__() - def notify_download(self, ep_name): - if sickbeard.GROWL_NOTIFY_ONDOWNLOAD: - self._sendGrowl(common.notifyStrings[common.NOTIFY_DOWNLOAD], ep_name) + self.sg_logo_file = 'apple-touch-icon-72x72.png' - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD: - self._sendGrowl(common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_GROWL: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendGrowl(title, update_text + new_version) + def _send_growl_msg(self, options, message=None): - def _send_growl(self, options, message=None): - - #Send Notification + # Send Notification notice = gntp.GNTPNotice() - #Required + # Required notice.add_header('Application-Name', options['app']) notice.add_header('Notification-Name', options['name']) notice.add_header('Notification-Title', options['title']) @@ -64,7 +47,7 @@ class GrowlNotifier: if options['password']: notice.set_password(options['password']) - #Optional + # Optional if options['sticky']: notice.add_header('Notification-Sticky', options['sticky']) if options['priority']: @@ -77,11 +60,15 @@ class GrowlNotifier: notice.add_header('Notification-Text', message) response = self._send(options['host'], options['port'], notice.encode(), options['debug']) - if isinstance(response, gntp.GNTPOK): return True + if isinstance(response, gntp.GNTPOK): + return True return False - def _send(self, host, port, data, debug=False): - if debug: print('\n', data, '\n') + @staticmethod + def _send(host, port, data, debug=False): + + if debug: + print('\n', data, '\n') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) @@ -89,107 +76,71 @@ class GrowlNotifier: response = gntp.parse_gntp(s.recv(1024)) s.close() - if debug: print('\n', response, '\n') + if debug: + print('\n', response, '\n') return response - def _sendGrowl(self, title='SickGear Notification', message=None, name=None, host=None, password=None, - force=False): - if not sickbeard.USE_GROWL and not force: - return False + def _send_registration(self, host=None, password=None): - if name == None: - name = title + host_parts = self._choose(host, sickbeard.GROWL_HOST).split(':') + port = 23053 if (2 != len(host_parts) or '' == host_parts[1]) else int(host_parts[1]) + password = self._choose(password, sickbeard.GROWL_PASSWORD) - if host == None: - hostParts = sickbeard.GROWL_HOST.split(':') - else: - hostParts = host.split(':') + opts = dict(app='SickGear', host=host_parts[0], port=port, password=password, debug=False) - if len(hostParts) != 2 or hostParts[1] == '': - port = 23053 - else: - port = int(hostParts[1]) - - growlHosts = [(hostParts[0], port)] - - opts = {} - - opts['name'] = name - - opts['title'] = title - opts['app'] = 'SickGear' - - opts['sticky'] = None - opts['priority'] = None - opts['debug'] = False - - if password == None: - opts['password'] = sickbeard.GROWL_PASSWORD - else: - opts['password'] = password - - opts['icon'] = True - - for pc in growlHosts: - opts['host'] = pc[0] - opts['port'] = pc[1] - logger.log(u'GROWL: Sending message "' + message + '" to ' + opts['host'] + ':' + str(opts['port']), logger.DEBUG) - try: - if self._send_growl(opts, message): - return True - else: - if self._sendRegistration(host, password, 'Sickbeard'): - return self._send_growl(opts, message) - else: - return False - except Exception as e: - logger.log(u'GROWL: Unable to send growl to ' + opts['host'] + ':' + str(opts['port']) + ' - ' + ex(e), logger.WARNING) - return False - - def _sendRegistration(self, host=None, password=None, name='SickGear Notification'): - opts = {} - - if host == None: - hostParts = sickbeard.GROWL_HOST.split(':') - else: - hostParts = host.split(':') - - if len(hostParts) != 2 or hostParts[1] == '': - port = 23053 - else: - port = int(hostParts[1]) - - opts['host'] = hostParts[0] - opts['port'] = port - - if password == None: - opts['password'] = sickbeard.GROWL_PASSWORD - else: - opts['password'] = password - - opts['app'] = 'SickGear' - opts['debug'] = False - - #Send Registration + # Send Registration register = gntp.GNTPRegister() register.add_header('Application-Name', opts['app']) - register.add_header('Application-Icon', - 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon-72x72.png') + register.add_header('Application-Icon', self._sg_logo_url) register.add_notification('Test', True) - register.add_notification(common.notifyStrings[common.NOTIFY_SNATCH], True) - register.add_notification(common.notifyStrings[common.NOTIFY_DOWNLOAD], True) - register.add_notification(common.notifyStrings[common.NOTIFY_GIT_UPDATE], True) - + register.add_notification(notify_strings['snatch'], True) + register.add_notification(notify_strings['download'], True) + register.add_notification(notify_strings['git_updated'], True) + if opts['password']: register.set_password(opts['password']) try: return self._send(opts['host'], opts['port'], register.encode(), opts['debug']) except Exception as e: - logger.log(u'GROWL: Unable to send growl to ' + opts['host'] + ':' + str(opts['port']) + ' - ' + ex(e), logger.WARNING) + self._log_warning(u'Unable to send growl to %s:%s - %s' % (opts['host'], opts['port'], ex(e))) return False + def _notify(self, title, body, name=None, host=None, password=None, **kwargs): + + name = name or title or 'SickGear Notification' + + host_parts = self._choose(host, sickbeard.GROWL_HOST).split(':') + port = (int(host_parts[1]), 23053)[len(host_parts) != 2 or '' == host_parts[1]] + growl_hosts = [(host_parts[0], port)] + password = self._choose(password, sickbeard.GROWL_PASSWORD) + + opts = dict(title=title, name=name, app='SickGear', sticky=None, priority=None, + password=password, icon=True, debug=False) + + for pc in growl_hosts: + opts['host'] = pc[0] + opts['port'] = pc[1] + try: + if self._send_growl_msg(opts, body): + return True + + if self._send_registration(host, password): + return self._send_growl_msg(opts, body) + + except Exception as e: + self._log_warning(u'Unable to send growl to %s:%s - %s' % (opts['host'], opts['port'], ex(e))) + + return False + + def test_notify(self, host, password): + self._testing = True + self._send_registration(host, password) + return ('Success, registered and tested', 'Failed registration and testing')[ + True is not super(GrowlNotifier, self).test_notify(name='Test', host=host, password=password)] + \ + (urllib.unquote_plus(host) + ' with password: ' + password, '')[password in (None, '')] + notifier = GrowlNotifier diff --git a/sickbeard/notifiers/kodi.py b/sickbeard/notifiers/kodi.py index c93a9752..b9c9e027 100644 --- a/sickbeard/notifiers/kodi.py +++ b/sickbeard/notifiers/kodi.py @@ -17,56 +17,29 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import time -import urllib - -import sickbeard -import sickbeard.helpers -from sickbeard.exceptions import ex -from sickbeard import logger, common - -try: - # noinspection PyPep8Naming - import xml.etree.cElementTree as etree -except ImportError: - # noinspection PyPep8Naming - import xml.etree.ElementTree as etree - try: import json except ImportError: from lib import simplejson as json +import time +import urllib +import xml.etree.cElementTree as XmlEtree + +import sickbeard +import sickbeard.helpers +from sickbeard import logger +from sickbeard.exceptions import ex +from sickbeard.notifiers.generic import Notifier -class KodiNotifier: +class KodiNotifier(Notifier): + def __init__(self): - self.sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/' + \ - 'apple-touch-icon-precomposed.png' + super(KodiNotifier, self).__init__() self.username, self.password = (None, None) self.response = None self.prefix = '' - self.test_mode = False - - @staticmethod - def _log(msg, log_level=logger.WARNING): - - logger.log(u'Kodi: %s' % msg, log_level) - - def _maybe_log(self, msg, log_level=logger.WARNING): - - if msg and (sickbeard.KODI_ALWAYS_ON or self.test_mode): - self._log(msg + (not sickbeard.KODI_ALWAYS_ON and self.test_mode and - ' (Test mode ignores "Always On")' or ''), log_level) - - def _maybe_log_failed_detection(self, host, msg='connect to'): - - self._maybe_log(u'Failed to %s %s, check device(s) and config.' % (msg, host), logger.ERROR) - - # noinspection PyUnusedLocal - def cb_response(self, r, *args, **kwargs): - self.response = dict(status_code=r.status_code) - return r def _get_kodi_version(self, host): """ Return Kodi JSON-RPC API version (odd # = dev, even # = stable) @@ -88,7 +61,7 @@ class KodiNotifier: """ timeout = 10 - response = self._send_to_kodi_json(host, dict(method='JSONRPC.Version'), timeout) + response = self._send_json(host, dict(method='JSONRPC.Version'), timeout) if self.response and 401 == self.response.get('status_code'): return False @@ -98,77 +71,19 @@ class KodiNotifier: # fallback to legacy HTTPAPI method test_command = {'command': 'Help'} - if self._send_to_kodi(host, test_command, timeout): + if self._send(host, test_command, timeout): # return fake version number to use the legacy method return 1 if self.response and 404 == self.response.get('status_code'): self.prefix = 'xbmc' - if self._send_to_kodi(host, test_command, timeout): + if self._send(host, test_command, timeout): # return fake version number to use the legacy method return 1 return False - def _notify_kodi(self, msg, title='SickGear', kodi_hosts=None): - """ Internal wrapper for the notify_snatch and notify_download functions - - Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. - - Args: - msg: Message body of the notice to send - title: Title of the notice to send - - Return: - A list of results in the format of host:ip:result, where result will either be 'OK' or False. - """ - - # fill in omitted parameters - if not kodi_hosts: - kodi_hosts = sickbeard.KODI_HOST - - if not sickbeard.USE_KODI and not self.test_mode: - self._log(u'Notification not enabled, skipping this notification', logger.DEBUG) - return False, None - - total_success = True - message = [] - for host in [x.strip() for x in kodi_hosts.split(',')]: - cur_host = urllib.unquote_plus(host) - - self._log(u'Sending notification to "%s" - %s' % (cur_host, message), logger.DEBUG) - - api_version = self._get_kodi_version(cur_host) - if self.response and 401 == self.response.get('status_code'): - total_success = False - message += ['Fail: Cannot authenticate with %s' % cur_host] - self._log(u'Failed to authenticate with %s' % cur_host, logger.DEBUG) - elif not api_version: - total_success = False - message += ['Fail: No supported Kodi found at %s' % cur_host] - self._maybe_log_failed_detection(cur_host, 'connect and detect version for') - else: - if 4 >= api_version: - self._log(u'Detected %sversion <= 11, using HTTP API' - % self.prefix and ' ' + self.prefix.capitalize(), logger.DEBUG) - __method_send = self._send_to_kodi - command = dict(command='ExecBuiltIn', - parameter='Notification(%s,%s)' % (title, msg)) - else: - self._log(u'Detected version >= 12, using JSON API', logger.DEBUG) - __method_send = self._send_to_kodi_json - command = dict(method='GUI.ShowNotification', - params={'title': '%s' % title, - 'message': '%s' % msg, - 'image': '%s' % self.sg_logo_url}) - - response_notify = __method_send(cur_host, command, 10) - if response_notify: - message += ['%s: %s' % ((response_notify, 'OK')['OK' in response_notify], cur_host)] - - return total_success, '
\n'.join(message) - - def _update_library(self, show_name=None): + def update_library(self, show_name=None, **kwargs): """ Wrapper for the update library functions Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. @@ -184,7 +99,7 @@ class KodiNotifier: Returns: True if processing succeeded with no issues else False if any issues found """ if not sickbeard.KODI_HOST: - self._log(u'No Kodi hosts specified, check your settings') + self._log_warning(u'No Kodi hosts specified, check your settings') return False # either update each host, or only attempt to update until one successful result @@ -196,15 +111,15 @@ class KodiNotifier: for cur_host in [x.strip() for x in sickbeard.KODI_HOST.split(',')]: - response = self._send_to_kodi_json(cur_host, dict(method='Profiles.GetCurrentProfile')) + response = self._send_json(cur_host, dict(method='Profiles.GetCurrentProfile')) if self.response and 401 == self.response.get('status_code'): - self._log(u'Failed to authenticate with %s' % cur_host, logger.DEBUG) + self._log_debug(u'Failed to authenticate with %s' % cur_host) continue if not response: self._maybe_log_failed_detection(cur_host) continue - if self._send_update_library(cur_host, show_name): + if self._send_library_update(cur_host, show_name): only_first.update(dict(profile=response.get('label') or 'Master', host=cur_host)) self._log('Success: profile;' + u'"%(profile)s" at%(first)s host;%(host)s updated%(show)s%(first_note)s' % only_first) @@ -218,7 +133,7 @@ class KodiNotifier: # needed for the 'update kodi' submenu command as it only cares of the final result vs the individual ones return 0 == result - def _send_update_library(self, host, show_name=None): + def _send_library_update(self, host, show_name=None): """ Internal wrapper for the update library function Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. @@ -229,9 +144,6 @@ class KodiNotifier: Return: True if the update was successful else False """ - - self._log(u'Sending request to update library for host: "%s"' % host, logger.DEBUG) - api_version = self._get_kodi_version(host) if api_version: # try to update just the show, if it fails, do full update if enabled @@ -241,18 +153,17 @@ class KodiNotifier: failed_msg = 'Single show update failed,' if sickbeard.KODI_UPDATE_FULL: - self._log(u'%s falling back to full update' % failed_msg, logger.DEBUG) + self._log_debug(u'%s falling back to full update' % failed_msg) return __method_update(host) - self._log(u'%s consider enabling "Perform full library update" in config/notifications' % failed_msg, - logger.DEBUG) + self._log_debug(u'%s consider enabling "Perform full library update" in config/notifications' % failed_msg) return False ############################################################################## # Legacy HTTP API (pre Kodi 12) methods ############################################################################## - def _send_to_kodi(self, host, command, timeout=30): + def _send(self, host, command, timeout=30): """ Handle communication to Kodi servers via HTTP API Args: @@ -263,11 +174,11 @@ class KodiNotifier: """ if not host: - self._log(u'No host specified, aborting update', logger.WARNING) + self._log_warning(u'No host specified, aborting update') return False args = {} - if not sickbeard.KODI_ALWAYS_ON and not self.test_mode: + if not sickbeard.KODI_ALWAYS_ON and not self._testing: args['mute_connect_err'] = True if self.password or sickbeard.KODI_PASSWORD: @@ -292,51 +203,54 @@ class KodiNotifier: """ if not host: - self._log(u'No host specified, aborting update', logger.WARNING) + self._log_warning(u'No host specified, aborting update') return False - self._log(u'Updating library via HTTP method for host: %s' % host, logger.DEBUG) + self._log_debug(u'Updating library via HTTP method for host: %s' % host) # if we're doing per-show if show_name: - self._log(u'Updating library via HTTP method for show %s' % show_name, logger.DEBUG) + self._log_debug(u'Updating library via HTTP method for show %s' % show_name) - path_sql = 'SELECT path.strPath FROM path, tvshow, tvshowlinkpath WHERE ' \ - 'tvshow.c00 = "%s"' % show_name \ - + ' AND tvshowlinkpath.idShow = tvshow.idShow AND tvshowlinkpath.idPath = path.idPath' + # noinspection SqlResolve + path_sql = 'SELECT path.strPath' \ + ' FROM path, tvshow, tvshowlinkpath' \ + ' WHERE tvshow.c00 = "%s"' % show_name \ + + ' AND tvshowlinkpath.idShow = tvshow.idShow' \ + ' AND tvshowlinkpath.idPath = path.idPath' # set xml response format, if this fails then don't bother with the rest - if not self._send_to_kodi( - host, {'command': 'SetResponseFormat(webheader;false;webfooter;false;header;;footer;;' + + if not self._send( + host, {'command': 'SetResponseFormat(webheader;false;webfooter;false;header;;footer;;' 'opentag;;closetag;;closefinaltag;false)'}): return False # sql used to grab path(s) - response = self._send_to_kodi(host, {'command': 'QueryVideoDatabase(%s)' % path_sql}) + response = self._send(host, {'command': 'QueryVideoDatabase(%s)' % path_sql}) if not response: - self._log(u'Invalid response for %s on %s' % (show_name, host), logger.DEBUG) + self._log_debug(u'Invalid response for %s on %s' % (show_name, host)) return False try: - et = etree.fromstring(urllib.quote(response, ':\\/<>')) + et = XmlEtree.fromstring(urllib.quote(response, ':\\/<>')) except SyntaxError as e: - self._log(u'Unable to parse XML in response: %s' % ex(e), logger.ERROR) + self._log_error(u'Unable to parse XML in response: %s' % ex(e)) return False paths = et.findall('.//field') if not paths: - self._log(u'No valid path found for %s on %s' % (show_name, host), logger.DEBUG) + self._log_debug(u'No valid path found for %s on %s' % (show_name, host)) return False for path in paths: # we do not need it double-encoded, gawd this is dumb un_enc_path = urllib.unquote(path.text).decode(sickbeard.SYS_ENCODING) - self._log(u'Updating %s on %s at %s' % (show_name, host, un_enc_path), logger.DEBUG) + self._log_debug(u'Updating %s on %s at %s' % (show_name, host, un_enc_path)) - if not self._send_to_kodi( - host, {'command': 'ExecBuiltIn', 'parameter': 'Kodi.updatelibrary(video, %s)' % un_enc_path}): - self._log(u'Update of show directory failed for %s on %s at %s' - % (show_name, host, un_enc_path), logger.ERROR) + if not self._send( + host, dict(command='ExecBuiltIn', parameter='Kodi.updatelibrary(video, %s)' % un_enc_path)): + self._log_error(u'Update of show directory failed for %s on %s at %s' + % (show_name, host, un_enc_path)) return False # sleep for a few seconds just to be sure kodi has a chance to finish each directory @@ -344,10 +258,10 @@ class KodiNotifier: time.sleep(5) # do a full update if requested else: - self._log(u'Full library update on host: %s' % host, logger.DEBUG) + self._log_debug(u'Full library update on host: %s' % host) - if not self._send_to_kodi(host, {'command': 'ExecBuiltIn', 'parameter': 'Kodi.updatelibrary(video)'}): - self._log(u'Failed full library update on: %s' % host, logger.ERROR) + if not self._send(host, dict(command='ExecBuiltIn', parameter='Kodi.updatelibrary(video)')): + self._log_error(u'Failed full library update on: %s' % host) return False return True @@ -356,7 +270,7 @@ class KodiNotifier: # JSON-RPC API (Kodi 12+) methods ############################################################################## - def _send_to_kodi_json(self, host, command, timeout=30): + def _send_json(self, host, command, timeout=30): """ Handle communication to Kodi installations via JSONRPC Args: @@ -368,7 +282,7 @@ class KodiNotifier: result = {} if not host: - self._log(u'No host specified, aborting update', logger.WARNING) + self._log_warning(u'No host specified, aborting update') return result if isinstance(command, dict): @@ -378,7 +292,7 @@ class KodiNotifier: else: args = dict(data=command) - if not sickbeard.KODI_ALWAYS_ON and not self.test_mode: + if not sickbeard.KODI_ALWAYS_ON and not self._testing: args['mute_connect_err'] = True if self.password or sickbeard.KODI_PASSWORD: @@ -391,8 +305,8 @@ class KodiNotifier: if not response.get('error'): return 'OK' == response.get('result') and {'OK': True} or response.get('result') - self._log(u'API error; %s from %s in response to command: %s' - % (json.dumps(response['error']), host, json.dumps(command)), logger.ERROR) + self._log_error(u'API error; %s from %s in response to command: %s' + % (json.dumps(response['error']), host, json.dumps(command))) return result def _update_json(self, host=None, show_name=None): @@ -408,12 +322,12 @@ class KodiNotifier: """ if not host: - self._log(u'No host specified, aborting update', logger.WARNING) + self._log_warning(u'No host specified, aborting update') return False # if we're doing per-show if show_name: - self._log(u'JSON library update. Host: %s Show: %s' % (host, show_name), logger.DEBUG) + self._log_debug(u'JSON library update. Host: %s Show: %s' % (host, show_name)) # try fetching tvshowid using show_name with a fallback to getting show list show_name = urllib.unquote_plus(show_name) @@ -424,17 +338,18 @@ class KodiNotifier: shows = None for command in commands: - response = self._send_to_kodi_json(host, command) + response = self._send_json(host, command) shows = response.get('tvshows') if shows: break if not shows: - self._log(u'No items in GetTVShows response', logger.DEBUG) + self._log_debug(u'No items in GetTVShows response') return False tvshowid = -1 path = '' + # noinspection PyTypeChecker for show in shows: if show_name == show.get('title') or show_name == show.get('label'): tvshowid = show.get('tvshowid', -1) @@ -444,82 +359,104 @@ class KodiNotifier: # we didn't find the show (exact match), thus revert to just doing a full update if enabled if -1 == tvshowid: - self._log(u'Doesn\'t have "%s" in it\'s known shows, full library update required' % show_name, - logger.DEBUG) + self._log_debug(u'Doesn\'t have "%s" in it\'s known shows, full library update required' % show_name) return False # lookup tv-show path if we don't already know it if not len(path): command = dict(method='VideoLibrary.GetTVShowDetails', params={'tvshowid': tvshowid, 'properties': ['file']}) - response = self._send_to_kodi_json(host, command) + response = self._send_json(host, command) path = 'tvshowdetails' in response and response['tvshowdetails'].get('file', '') or '' if not len(path): - self._log(u'No valid path found for %s with ID: %s on %s' % (show_name, tvshowid, host), logger.WARNING) + self._log_warning(u'No valid path found for %s with ID: %s on %s' % (show_name, tvshowid, host)) return False - self._log(u'Updating %s on %s at %s' % (show_name, host, path), logger.DEBUG) + self._log_debug(u'Updating %s on %s at %s' % (show_name, host, path)) command = dict(method='VideoLibrary.Scan', params={'directory': '%s' % json.dumps(path)[1:-1]}) - response_scan = self._send_to_kodi_json(host, command) + response_scan = self._send_json(host, command) if not response_scan.get('OK'): - self._log(u'Update of show directory failed for %s on %s at %s response: %s' % - (show_name, host, path, response_scan), logger.ERROR) + self._log_error(u'Update of show directory failed for %s on %s at %s response: %s' % + (show_name, host, path, response_scan)) return False # do a full update if requested else: - self._log(u'Full library update on host: %s' % host, logger.DEBUG) - response_scan = self._send_to_kodi_json(host, dict(method='VideoLibrary.Scan')) + self._log_debug(u'Full library update on host: %s' % host) + response_scan = self._send_json(host, dict(method='VideoLibrary.Scan')) if not response_scan.get('OK'): - self._log(u'Failed full library update on: %s response: %s' % (host, response_scan), logger.ERROR) + self._log_error(u'Failed full library update on: %s response: %s' % (host, response_scan)) return False return True - ############################################################################## - # Public functions which will call the JSON or Legacy HTTP API methods - ############################################################################## + # noinspection PyUnusedLocal + def cb_response(self, r, *args, **kwargs): + self.response = dict(status_code=r.status_code) + return r - def notify_snatch(self, ep_name): + def _maybe_log(self, msg, log_level=logger.WARNING): - if sickbeard.KODI_NOTIFY_ONSNATCH: - self._notify_kodi(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) + if msg and (sickbeard.KODI_ALWAYS_ON or self._testing): + self._log(msg + (not sickbeard.KODI_ALWAYS_ON and self._testing and + ' (Test mode ignores "Always On")' or ''), log_level) - def notify_download(self, ep_name): + def _maybe_log_failed_detection(self, host, msg='connect to'): - if sickbeard.KODI_NOTIFY_ONDOWNLOAD: - self._notify_kodi(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD]) + self._maybe_log(u'Failed to %s %s, check device(s) and config' % (msg, host), logger.ERROR) - def notify_subtitle_download(self, ep_name, lang): + def _notify(self, title, body, hosts, username, password, **kwargs): + """ Internal wrapper for the notify_snatch and notify_download functions - if sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notify_kodi('%s: %s' % (ep_name, lang), common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) + Call either the JSON-RPC over HTTP or the legacy HTTP API methods depending on the Kodi API version. - def notify_git_update(self, new_version='??'): + Args: + title: Title of the notice to send + body: Message body of the notice to send - if sickbeard.USE_KODI: - update_text = common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._notify_kodi('%s %s' % (update_text, new_version), title) - - def test_notify(self, host, username, password): - - self.test_mode, self.username, self.password = True, username, password - result = self._notify_kodi('Testing SickGear Kodi notifier', 'Test Notification', kodi_hosts=host) - self.test_mode = False - return result - - def update_library(self, showName=None, force=False): - """ Wrapper for the update library functions - - :param showName: Name of a TV show - :param force: True force update process - - Returns: None if no processing done, True if processing succeeded with no issues else False if any issues found + Return: + A list of results in the format of host:ip:result, where result will either be 'OK' or False. """ - if sickbeard.USE_KODI and (sickbeard.KODI_UPDATE_LIBRARY or force): - return self._update_library(showName) + self.username, self.password = username, password + + title = title or 'SickGear' + + hosts = self._choose(hosts, sickbeard.KODI_HOST) + + success = True + message = [] + for host in [x.strip() for x in hosts.split(',')]: + cur_host = urllib.unquote_plus(host) + + api_version = self._get_kodi_version(cur_host) + if self.response and 401 == self.response.get('status_code'): + success = False + message += ['Fail: Cannot authenticate with %s' % cur_host] + self._log_debug(u'Failed to authenticate with %s' % cur_host) + elif not api_version: + success = False + message += ['Fail: No supported Kodi found at %s' % cur_host] + self._maybe_log_failed_detection(cur_host, 'connect and detect version for') + else: + if 4 >= api_version: + self._log_debug(u'Detected %sversion <= 11, using HTTP API' + % self.prefix and ' ' + self.prefix.capitalize()) + __method_send = self._send + command = dict(command='ExecBuiltIn', + parameter='Notification(%s,%s)' % (title, body)) + else: + self._log_debug(u'Detected version >= 12, using JSON API') + __method_send = self._send_json + command = dict(method='GUI.ShowNotification', params=dict( + [('title', title), ('message', body), ('image', self._sg_logo_url)] + + ([], [('displaytime', 8000)])[self._testing])) + + response_notify = __method_send(cur_host, command, 10) + if response_notify: + message += ['%s: %s' % ((response_notify, 'OK')['OK' in response_notify], cur_host)] + + return self._choose(('Success, all hosts tested', '
\n'.join(message))[not success], success) notifier = KodiNotifier diff --git a/sickbeard/notifiers/libnotify.py b/sickbeard/notifiers/libnotify.py index 3ac4dc48..0cd36f7c 100644 --- a/sickbeard/notifiers/libnotify.py +++ b/sickbeard/notifiers/libnotify.py @@ -20,23 +20,24 @@ import os import cgi import sickbeard -from sickbeard import logger, common +from sickbeard.notifiers.generic import Notifier def diagnose(): - ''' + """ Check the environment for reasons libnotify isn't working. Return a user-readable message indicating possible issues. - ''' + """ try: - import pynotify #@UnusedImport + # noinspection PyPackageRequirements + import pynotify except ImportError: - return (u"

Error: pynotify isn't installed. On Ubuntu/Debian, install the " - u"python-notify package.") + return ('Error: pynotify isn\'t installed. On Ubuntu/Debian, install the ' + 'python-notify package.') if 'DISPLAY' not in os.environ and 'DBUS_SESSION_BUS_ADDRESS' not in os.environ: - return (u"

Error: Environment variables DISPLAY and DBUS_SESSION_BUS_ADDRESS " - u"aren't set. libnotify will only work when you run SickGear " - u"from a desktop login.") + return ('Error: Environment variables DISPLAY and DBUS_SESSION_BUS_ADDRESS ' + 'aren\'t set. libnotify will only work when you run SickGear ' + 'from a desktop login.') try: import dbus except ImportError: @@ -45,82 +46,71 @@ def diagnose(): try: bus = dbus.SessionBus() except dbus.DBusException as e: - return (u"

Error: unable to connect to D-Bus session bus: %s." - u"

Are you running SickGear in a desktop session?") % (cgi.escape(e),) + return (u'Error: unable to connect to D-Bus session bus: %s. ' + u'Are you running SickGear in a desktop session?') % (cgi.escape(e),) try: bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications') except dbus.DBusException as e: - return (u"

Error: there doesn't seem to be a notification daemon available: %s " - u"

Try installing notification-daemon or notify-osd.") % (cgi.escape(e),) - return u"

Error: Unable to send notification." + return (u'Error: there doesn\'t seem to be a notification daemon available: %s ' + u'Try installing notification-daemon or notify-osd.') % (cgi.escape(e),) + return 'Error: Unable to send notification.' -class LibnotifyNotifier: +class LibnotifyNotifier(Notifier): + def __init__(self): + super(LibnotifyNotifier, self).__init__() + self.pynotify = None self.gobject = None def init_pynotify(self): if self.pynotify is not None: return True + try: + # noinspection PyPackageRequirements import pynotify except ImportError: - logger.log(u"Unable to import pynotify. libnotify notifications won't work.", logger.ERROR) + self._log_error(u'Unable to import pynotify. libnotify notifications won\'t work') return False + try: + # noinspection PyPackageRequirements from gi.repository import GObject except ImportError: - logger.log(u"Unable to import GObject from gi.repository. We can't catch a GError in display.", logger.ERROR) + self._log_error(u'Unable to import GObject from gi.repository. Cannot catch a GError in display') return False + if not pynotify.init('SickGear'): - logger.log(u"Initialization of pynotify failed. libnotify notifications won't work.", logger.ERROR) + self._log_error(u'Initialization of pynotify failed. libnotify notifications won\'t work') return False + self.pynotify = pynotify self.gobject = GObject return True - def notify_snatch(self, ep_name): - if sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH: - self._notify(common.notifyStrings[common.NOTIFY_SNATCH], ep_name) + def _notify(self, title, body, **kwargs): - def notify_download(self, ep_name): - if sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD: - self._notify(common.notifyStrings[common.NOTIFY_DOWNLOAD], ep_name) + result = False + if self.init_pynotify(): - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notify(common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ": " + lang) + # Can't make this a global constant because PROG_DIR isn't available + # when the module is imported. + icon_path = os.path.join(sickbeard.PROG_DIR, 'data/images/sickbeard_touch_icon.png') + icon_uri = 'file://' + os.path.abspath(icon_path) - def notify_git_update(self, new_version = "??"): - if sickbeard.USE_LIBNOTIFY: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._notify(title, update_text + new_version) + # If the session bus can't be acquired here a bunch of warning messages + # will be printed but the call to show() will still return True. + # pynotify doesn't seem too keen on error handling. + n = self.pynotify.Notification(title, body, icon_uri) + try: + result = n.show() + except self.gobject.GError: + pass - def test_notify(self): - return self._notify('Test notification', "This is a test notification from SickGear", force=True) - - def _notify(self, title, message, force=False): - if not sickbeard.USE_LIBNOTIFY and not force: - return False - if not self.init_pynotify(): - return False - - # Can't make this a global constant because PROG_DIR isn't available - # when the module is imported. - icon_path = os.path.join(sickbeard.PROG_DIR, "data/images/sickbeard_touch_icon.png") - icon_uri = 'file://' + os.path.abspath(icon_path) - - # If the session bus can't be acquired here a bunch of warning messages - # will be printed but the call to show() will still return True. - # pynotify doesn't seem too keen on error handling. - n = self.pynotify.Notification(title, message, icon_uri) - try: - return n.show() - except self.gobject.GError: - return False + return self._choose((True if result else diagnose()), result) notifier = LibnotifyNotifier diff --git a/sickbeard/notifiers/nma.py b/sickbeard/notifiers/nma.py index 2440a799..3ed63278 100644 --- a/sickbeard/notifiers/nma.py +++ b/sickbeard/notifiers/nma.py @@ -1,47 +1,15 @@ import sickbeard +from sickbeard.notifiers.generic import Notifier -from sickbeard import logger, common from lib.pynma import pynma -class NMA_Notifier: - def test_notify(self, nma_api, nma_priority): - return self._sendNMA(nma_api, nma_priority, event='Test', message='Testing NMA settings from SickGear', - force=True) +class NMANotifier(Notifier): - def notify_snatch(self, ep_name): - if sickbeard.NMA_NOTIFY_ONSNATCH: - self._sendNMA(nma_api=None, nma_priority=None, event=common.notifyStrings[common.NOTIFY_SNATCH], - message=ep_name) + def _notify(self, title, body, nma_api=None, nma_priority=None, **kwargs): - def notify_download(self, ep_name): - if sickbeard.NMA_NOTIFY_ONDOWNLOAD: - self._sendNMA(nma_api=None, nma_priority=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD], - message=ep_name) - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD: - self._sendNMA(nma_api=None, nma_priority=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], - message=ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_NMA: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendNMA(nma_api=None, nma_priority=None, event=title, message=update_text + new_version) - - def _sendNMA(self, nma_api=None, nma_priority=None, event=None, message=None, force=False): - - title = 'SickGear' - - if not sickbeard.USE_NMA and not force: - return False - - if nma_api == None: - nma_api = sickbeard.NMA_API - - if nma_priority == None: - nma_priority = sickbeard.NMA_PRIORITY + nma_api = self._choose(nma_api, sickbeard.NMA_API) + nma_priority = self._choose(nma_priority, sickbeard.NMA_PRIORITY) batch = False @@ -49,17 +17,22 @@ class NMA_Notifier: keys = nma_api.split(',') p.addkey(keys) - if len(keys) > 1: batch = True + if 1 < len(keys): + batch = True - logger.log('NMA: Sending notice with details: event="%s", message="%s", priority=%s, batch=%s' % (event, message, nma_priority, batch), logger.DEBUG) - response = p.push(title, event, message, priority=nma_priority, batch_mode=batch) + self._log_debug('Sending notice with priority=%s, batch=%s' % (nma_priority, batch)) + response = p.push('SickGear', title, body, priority=nma_priority, batch_mode=batch) - if not response[nma_api][u'code'] == u'200': - logger.log(u'Could not send notification to NotifyMyAndroid', logger.ERROR) - return False - else: - logger.log(u'NMA: Notification sent to NotifyMyAndroid', logger.MESSAGE) - return True + result = False + try: + if u'200' != response[nma_api][u'code']: + self._log_error('Notification failed') + else: + result = True + except (StandardError, Exception): + pass + + return result -notifier = NMA_Notifier +notifier = NMANotifier diff --git a/sickbeard/notifiers/nmj.py b/sickbeard/notifiers/nmj.py index 016c0175..6e00672e 100644 --- a/sickbeard/notifiers/nmj.py +++ b/sickbeard/notifiers/nmj.py @@ -16,188 +16,159 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import urllib, urllib2 -import sickbeard -import telnetlib import re +import telnetlib +import urllib +import urllib2 +import xml.etree.cElementTree as XmlEtree -from sickbeard import logger +import sickbeard from sickbeard.exceptions import ex - -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree +from sickbeard.notifiers.generic import BaseNotifier -class NMJNotifier: +class NMJNotifier(BaseNotifier): + def notify_settings(self, host): """ Retrieves the settings from a NMJ/Popcorn hour - + host: The hostname/IP of the Popcorn Hour server - + Returns: True if the settings were retrieved successfully, False otherwise """ # establish a terminal session to the PC - terminal = False + result, terminal = False, None try: terminal = telnetlib.Telnet(host) - except Exception: - logger.log(u'Warning: unable to get a telnet session to %s' % (host), logger.WARNING) - return False + except (StandardError, Exception): + self._log_warning(u'Unable to get a telnet session to %s' % host) - # tell the terminal to output the necessary info to the screen so we can search it later - logger.log(u'Connected to %s via telnet' % (host), logger.DEBUG) - terminal.read_until('sh-3.00# ') - terminal.write('cat /tmp/source\n') - terminal.write('cat /tmp/netshare\n') - terminal.write('exit\n') - tnoutput = terminal.read_all() + if result: + # tell the terminal to output the necessary info to the screen so we can search it later + self._log_debug(u'Connected to %s via telnet' % host) + terminal.read_until('sh-3.00# ') + terminal.write('cat /tmp/source\n') + terminal.write('cat /tmp/netshare\n') + terminal.write('exit\n') + tnoutput = terminal.read_all() - database = '' - device = '' - match = re.search(r'(.+\.db)\r\n?(.+)(?=sh-3.00# cat /tmp/netshare)', tnoutput) - - # if we found the database in the terminal output then save that database to the config - if match: - database = match.group(1) - device = match.group(2) - logger.log(u'Found NMJ database %s on device %s' % (database, device), logger.DEBUG) - sickbeard.NMJ_DATABASE = database - else: - logger.log(u'Could not get current NMJ database on %s, NMJ is probably not running!' % (host), logger.WARNING) - return False - - # if the device is a remote host then try to parse the mounting URL and save it to the config - if device.startswith('NETWORK_SHARE/'): - match = re.search('.*(?=\r\n?%s)' % (re.escape(device[14:])), tnoutput) - - if match: - mount = match.group().replace('127.0.0.1', host) - logger.log(u'Found mounting url on the Popcorn Hour in configuration: %s' % (mount), logger.DEBUG) - sickbeard.NMJ_MOUNT = mount + match = re.search(r'(.+\.db)\r\n?(.+)(?=sh-3.00# cat /tmp/netshare)', tnoutput) + # if we found the database in the terminal output then save that database to the config + if not match: + self._log_warning(u'Could not get current NMJ database on %s, NMJ is probably not running!' % host) else: - logger.log(u'Detected a network share on the Popcorn Hour, but could not get the mounting url', - logger.WARNING) - return False + database = match.group(1) + device = match.group(2) + self._log_debug(u'Found NMJ database %s on device %s' % (database, device)) + sickbeard.NMJ_DATABASE = database + # if the device is a remote host then try to parse the mounting URL and save it to the config + if device.startswith('NETWORK_SHARE/'): + match = re.search('.*(?=\r\n?%s)' % (re.escape(device[14:])), tnoutput) - return True + if not match: + self._log_warning('Detected a network share on the Popcorn Hour, ' + 'but could not get the mounting url') + else: + mount = match.group().replace('127.0.0.1', host) + self._log_debug(u'Found mounting url on the Popcorn Hour in configuration: %s' % mount) + sickbeard.NMJ_MOUNT = mount + result = True - def notify_snatch(self, ep_name): - return False - #Not implemented: Start the scanner when snatched does not make any sense + if result: + return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % { + "host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} + return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. ' \ + '(see Error Log -> Debug for detailed info)", "database": "", "mount": ""}' - def notify_download(self, ep_name): - if sickbeard.USE_NMJ: - self._notifyNMJ() - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.USE_NMJ: - self._notifyNMJ() - - def notify_git_update(self, new_version): - return False - # Not implemented, no reason to start scanner. - - def test_notify(self, host, database, mount): - return self._sendNMJ(host, database, mount) - - def _sendNMJ(self, host, database, mount=None): + def _send(self, host=None, database=None, mount=None): """ Sends a NMJ update command to the specified machine - + host: The hostname/IP to send the request to (no port) database: The database to send the requst to mount: The mount URL to use (optional) - + Returns: True if the request succeeded, False otherwise """ + host = self._choose(host, sickbeard.NMJ_HOST) + database = self._choose(database, sickbeard.NMJ_DATABASE) + mount = self._choose(mount, sickbeard.NMJ_MOUNT) + + self._log_debug(u'Sending scan command for NMJ ') # if a mount URL is provided then attempt to open a handle to that URL if mount: try: req = urllib2.Request(mount) - logger.log(u'Try to mount network drive via url: %s' % (mount), logger.DEBUG) - handle = urllib2.urlopen(req) + self._log_debug(u'Try to mount network drive via url: %s' % mount) + urllib2.urlopen(req) except IOError as e: if hasattr(e, 'reason'): - logger.log(u'NMJ: Could not contact Popcorn Hour on host %s: %s' % (host, e.reason), logger.WARNING) + self._log_warning(u'Could not contact Popcorn Hour on host %s: %s' % (host, e.reason)) elif hasattr(e, 'code'): - logger.log(u'NMJ: Problem with Popcorn Hour on host %s: %s' % (host, e.code), logger.WARNING) + self._log_warning(u'Problem with Popcorn Hour on host %s: %s' % (host, e.code)) return False except Exception as e: - logger.log(u'NMJ: Unknown exception: ' + ex(e), logger.ERROR) + self._log_error(u'Unknown exception: ' + ex(e)) return False # build up the request URL and parameters - UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s' - params = { - 'arg0': 'scanner_start', - 'arg1': database, - 'arg2': 'background', - 'arg3': '' - } + params = dict(arg0='scanner_start', arg1=database, arg2='background', arg3='') params = urllib.urlencode(params) - updateUrl = UPDATE_URL % {'host': host, 'params': params} + update_url = 'http://%(host)s:8008/metadata_database?%(params)s' % {'host': host, 'params': params} # send the request to the server try: - req = urllib2.Request(updateUrl) - logger.log(u'Sending NMJ scan update command via url: %s' % (updateUrl), logger.DEBUG) + req = urllib2.Request(update_url) + self._log_debug(u'Sending scan update command via url: %s' % update_url) handle = urllib2.urlopen(req) response = handle.read() except IOError as e: if hasattr(e, 'reason'): - logger.log(u'NMJ: Could not contact Popcorn Hour on host %s: %s' % (host, e.reason), logger.WARNING) + self._log_warning(u'Could not contact Popcorn Hour on host %s: %s' % (host, e.reason)) elif hasattr(e, 'code'): - logger.log(u'NMJ: Problem with Popcorn Hour on host %s: %s' % (host, e.code), logger.WARNING) + self._log_warning(u'Problem with Popcorn Hour on host %s: %s' % (host, e.code)) return False except Exception as e: - logger.log(u'NMJ: Unknown exception: ' + ex(e), logger.ERROR) + self._log_error(u'Unknown exception: ' + ex(e)) return False # try to parse the resulting XML try: - et = etree.fromstring(response) + et = XmlEtree.fromstring(response) result = et.findtext('returnValue') except SyntaxError as e: - logger.log(u'Unable to parse XML returned from the Popcorn Hour: %s' % (e), logger.ERROR) + self._log_error(u'Unable to parse XML returned from the Popcorn Hour: %s' % e) return False # if the result was a number then consider that an error - if int(result) > 0: - logger.log(u'Popcorn Hour returned an errorcode: %s' % (result), logger.ERROR) - return False - else: - logger.log(u'NMJ started background scan', logger.MESSAGE) - return True - - def _notifyNMJ(self, host=None, database=None, mount=None, force=False): - """ - Sends a NMJ update command based on the SB config settings - - host: The host to send the command to (optional, defaults to the host in the config) - database: The database to use (optional, defaults to the database in the config) - mount: The mount URL (optional, defaults to the mount URL in the config) - force: If True then the notification will be sent even if NMJ is disabled in the config - """ - if not sickbeard.USE_NMJ and not force: - logger.log('Notification for NMJ scan update not enabled, skipping this notification', logger.DEBUG) + if 0 < int(result): + self._log_error(u'Popcorn Hour returned an errorcode: %s' % result) return False - # fill in omitted parameters - if not host: - host = sickbeard.NMJ_HOST - if not database: - database = sickbeard.NMJ_DATABASE - if not mount: - mount = sickbeard.NMJ_MOUNT + self._log(u'NMJ started background scan') + return True - logger.log(u'Sending scan command for NMJ ', logger.DEBUG) + def _notify(self, host=None, database=None, mount=None, **kwargs): - return self._sendNMJ(host, database, mount) + result = self._send(host, database, mount) + + return self._choose((('Success, started %s', 'Failed to start %s')[not result] % 'the scan update'), result) + + def test_notify(self, host, database, mount): + self._testing = True + return self._notify(host, database, mount) + + # notify_snatch() Not implemented: Start the scanner when snatched does not make sense + # notify_git_update() Not implemented, no reason to start scanner + + def notify_download(self): + self._notify() + + def notify_subtitle_download(self): + self._notify() notifier = NMJNotifier diff --git a/sickbeard/notifiers/nmjv2.py b/sickbeard/notifiers/nmjv2.py index 026df235..889ce5ff 100644 --- a/sickbeard/notifiers/nmjv2.py +++ b/sickbeard/notifiers/nmjv2.py @@ -17,122 +17,129 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import urllib, urllib2, xml.dom.minidom -from xml.dom.minidom import parseString -import sickbeard -import telnetlib import re +import telnetlib import time +import urllib +import urllib2 +import xml.dom.minidom +from xml.dom.minidom import parseString +import xml.etree.cElementTree as XmlEtree -from sickbeard import logger - -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree +import sickbeard +from sickbeard.notifiers.generic import BaseNotifier -class NMJv2Notifier: - def notify_snatch(self, ep_name): - return False - #Not implemented: Start the scanner when snatched does not make any sense +class NMJv2Notifier(BaseNotifier): - def notify_download(self, ep_name): - self._notifyNMJ() - - def notify_subtitle_download(self, ep_name, lang): - self._notifyNMJ() - - def notify_git_update(self, new_version): - return False - # Not implemented, no reason to start scanner. - - def test_notify(self, host): - return self._sendNMJ(host) - - def notify_settings(self, host, dbloc, instance): + def notify_settings(self, host, db_loc, instance): """ Retrieves the NMJv2 database location from Popcorn hour - + host: The hostname/IP of the Popcorn Hour server dbloc: 'local' for PCH internal harddrive. 'network' for PCH network shares instance: Allows for selection of different DB in case of multiple databases - + Returns: True if the settings were retrieved successfully, False otherwise """ + result = False try: - url_loc = 'http://' + host + ':8008/file_operation?arg0=list_user_storage_file&arg1=&arg2=' + instance + '&arg3=20&arg4=true&arg5=true&arg6=true&arg7=all&arg8=name_asc&arg9=false&arg10=false' - req = urllib2.Request(url_loc) - handle1 = urllib2.urlopen(req) - response1 = handle1.read() - xml = parseString(response1) + base_url = 'http://%s:8008/' % host + + req = urllib2.Request('%s%s%s' % (base_url, 'file_operation?', urllib.urlencode( + dict(arg0='list_user_storage_file', arg1='', arg2=instance, arg3=20, arg4='true', arg5='true', + arg6='true', arg7='all', arg8='name_asc', arg9='false', arg10='false')))) + handle = urllib2.urlopen(req) + response = handle.read() + xml_data = parseString(response) + time.sleep(300.0 / 1000.0) - for node in xml.getElementsByTagName('path'): - xmlTag = node.toxml(); - xmlData = xmlTag.replace('', '').replace('', '').replace('[=]', '') - url_db = 'http://' + host + ':8008/metadata_database?arg0=check_database&arg1=' + xmlData - reqdb = urllib2.Request(url_db) + for node in xml_data.getElementsByTagName('path'): + xml_tag = node.toxml() + + reqdb = urllib2.Request('%s%s%s' % (base_url, 'metadata_database?', urllib.urlencode( + dict(arg0='check_database', + arg1=xml_tag.replace('', '').replace('', '').replace('[=]', ''))))) handledb = urllib2.urlopen(reqdb) responsedb = handledb.read() - xmldb = parseString(responsedb) - returnvalue = xmldb.getElementsByTagName('returnValue')[0].toxml().replace('', '').replace( - '', '') - if returnvalue == '0': - DB_path = xmldb.getElementsByTagName('database_path')[0].toxml().replace('', - '').replace( - '', '').replace('[=]', '') - if dbloc == 'local' and DB_path.find('localhost') > -1: + xml_db = parseString(responsedb) + + if '0' == xml_db.getElementsByTagName('returnValue')[0].toxml().replace( + '', '').replace('', ''): + db_path = xml_db.getElementsByTagName('database_path')[0].toxml().replace( + '', '').replace('', '').replace('[=]', '') + if 'local' == db_loc and db_path.find('localhost') > -1: sickbeard.NMJv2_HOST = host - sickbeard.NMJv2_DATABASE = DB_path - return True - if dbloc == 'network' and DB_path.find('://') > -1: + sickbeard.NMJv2_DATABASE = db_path + result = True + if 'network' == db_loc and db_path.find('://') > -1: sickbeard.NMJv2_HOST = host - sickbeard.NMJv2_DATABASE = DB_path - return True + sickbeard.NMJv2_DATABASE = db_path + result = True except IOError as e: - logger.log(u"Warning: Couldn't contact popcorn hour on host %s: %s" % (host, e), logger.WARNING) - return False - return False + self._log_warning(u'Couldn\'t contact popcorn hour on host %s: %s' % (host, e)) - def _sendNMJ(self, host): + if result: + return '{"message": "Success, NMJ Database found at: %(host)s", "database": "%(database)s"}' % { + "host": host, "database": sickbeard.NMJv2_DATABASE} + + return '{"message": "Failed to find NMJ Database at location: %(dbloc)s. ' \ + 'Is the right location selected and PCH running? ", "database": ""}' % {"dbloc": db_loc} + + def _send(self, host=None): """ Sends a NMJ update command to the specified machine - + host: The hostname/IP to send the request to (no port) database: The database to send the requst to mount: The mount URL to use (optional) - + Returns: True if the request succeeded, False otherwise """ - #if a host is provided then attempt to open a handle to that URL + host = self._choose(host, sickbeard.NMJv2_HOST) + + self._log_debug(u'Sending scan command for NMJ ') + + # if a host is provided then attempt to open a handle to that URL try: - url_scandir = 'http://' + host + ':8008/metadata_database?arg0=update_scandir&arg1=' + sickbeard.NMJv2_DATABASE + '&arg2=&arg3=update_all' - logger.log(u'NMJ scan update command sent to host: %s' % (host), logger.DEBUG) - url_updatedb = 'http://' + host + ':8008/metadata_database?arg0=scanner_start&arg1=' + sickbeard.NMJv2_DATABASE + '&arg2=background&arg3=' - logger.log(u'Try to mount network drive via url: %s' % (host), logger.DEBUG) + base_url = 'http://%s:8008/' % host + + url_scandir = '%s%s%s' % (base_url, 'metadata_database?', urllib.urlencode( + dict(arg0='update_scandir', arg1=sickbeard.NMJv2_DATABASE, arg2='', arg3='update_all'))) + self._log_debug(u'Scan update command sent to host: %s' % host) + + url_updatedb = '%s%s%s' % (base_url, 'metadata_database?', urllib.urlencode( + dict(arg0='scanner_start', arg1=sickbeard.NMJv2_DATABASE, arg2='background', arg3=''))) + self._log_debug(u'Try to mount network drive via url: %s' % host) + prereq = urllib2.Request(url_scandir) req = urllib2.Request(url_updatedb) + handle1 = urllib2.urlopen(prereq) response1 = handle1.read() + time.sleep(300.0 / 1000.0) + handle2 = urllib2.urlopen(req) response2 = handle2.read() except IOError as e: - logger.log(u"Warning: Couldn't contact popcorn hour on host %s: %s" % (host, e), logger.WARNING) + self._log_warning(u'Couldn\'t contact popcorn hour on host %s: %s' % (host, e)) return False + try: - et = etree.fromstring(response1) + et = XmlEtree.fromstring(response1) result1 = et.findtext('returnValue') except SyntaxError as e: - logger.log(u'Unable to parse XML returned from the Popcorn Hour: update_scandir, %s' % (e), logger.ERROR) + self._log_error(u'Unable to parse XML returned from the Popcorn Hour: update_scandir, %s' % e) return False + try: - et = etree.fromstring(response2) + et = XmlEtree.fromstring(response2) result2 = et.findtext('returnValue') except SyntaxError as e: - logger.log(u'Unable to parse XML returned from the Popcorn Hour: scanner_start, %s' % (e), logger.ERROR) + self._log_error(u'Unable to parse XML returned from the Popcorn Hour: scanner_start, %s' % e) return False # if the result was a number then consider that an error @@ -144,39 +151,38 @@ class NMJv2Notifier: 'Database read error', 'Open fifo pipe failed', 'Read only file system'] - if int(result1) > 0: + if 0 < int(result1): index = error_codes.index(result1) - logger.log(u'Popcorn Hour returned an error: %s' % (error_messages[index]), logger.ERROR) - return False - else: - if int(result2) > 0: - index = error_codes.index(result2) - logger.log(u'Popcorn Hour returned an error: %s' % (error_messages[index]), logger.ERROR) - return False - else: - logger.log(u'NMJv2 started background scan', logger.MESSAGE) - return True - - def _notifyNMJ(self, host=None, force=False): - """ - Sends a NMJ update command based on the SB config settings - - host: The host to send the command to (optional, defaults to the host in the config) - database: The database to use (optional, defaults to the database in the config) - mount: The mount URL (optional, defaults to the mount URL in the config) - force: If True then the notification will be sent even if NMJ is disabled in the config - """ - if not sickbeard.USE_NMJv2 and not force: - logger.log('Notification for NMJ scan update not enabled, skipping this notification', logger.DEBUG) + self._log_error(u'Popcorn Hour returned an error: %s' % (error_messages[index])) return False - # fill in omitted parameters - if not host: - host = sickbeard.NMJv2_HOST + elif 0 < int(result2): + index = error_codes.index(result2) + self._log_error(u'Popcorn Hour returned an error: %s' % (error_messages[index])) + return False - logger.log(u'Sending scan command for NMJ ', logger.DEBUG) + self._log(u'NMJv2 started background scan') + return True - return self._sendNMJ(host) + def _notify(self, host=None, **kwargs): + + result = self._send(host) + + return self._choose((('Success, started %s', 'Failed to start %s')[not result] % 'the scan update at "%s"' + % host), result) + + def test_notify(self, host): + self._testing = True + return self._notify(host) + + # notify_snatch() Not implemented: Start the scanner when snatched does not make sense + # notify_git_update() Not implemented, no reason to start scanner + + def notify_download(self): + self._notify() + + def notify_subtitle_download(self): + self._notify() notifier = NMJv2Notifier diff --git a/sickbeard/notifiers/plex.py b/sickbeard/notifiers/plex.py index c6fd6f51..b6364bff 100644 --- a/sickbeard/notifiers/plex.py +++ b/sickbeard/notifiers/plex.py @@ -20,28 +20,18 @@ import urllib import urllib2 import base64 import re +import xml.etree.cElementTree as XmlEtree import sickbeard - -from sickbeard import common, logger -from sickbeard.exceptions import ex from sickbeard.encodingKludge import fixStupidEncodings - -try: - import xml.etree.cElementTree as etree -except ImportError: - import elementtree.ElementTree as etree +from sickbeard.exceptions import ex +from sickbeard.notifiers.generic import Notifier -class PLEXNotifier: +class PLEXNotifier(Notifier): def __init__(self): - - self.name = 'PLEX' - - def log(self, msg, level=logger.MESSAGE): - - logger.log(u'%s: %s' % (self.name, msg), level) + super(PLEXNotifier, self).__init__() def _send_to_plex(self, command, host, username=None, password=None): """Handles communication to Plex hosts via HTTP API @@ -53,18 +43,11 @@ class PLEXNotifier: password: Plex API password Returns: - Returns 'OK' for successful commands or False if there was an error + Returns True for successful commands or False if there was an error """ - - # fill in omitted parameters - if not username: - username = sickbeard.PLEX_USERNAME - if not password: - password = sickbeard.PLEX_PASSWORD - if not host: - self.log(u'No host specified, check your settings', logger.ERROR) + self._log_error(u'No host specified, check your settings') return False for key in command: @@ -72,102 +55,30 @@ class PLEXNotifier: command[key] = command[key].encode('utf-8') enc_command = urllib.urlencode(command) - self.log(u'Encoded API command: ' + enc_command, logger.DEBUG) + self._log_debug(u'Encoded API command: ' + enc_command) url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, enc_command) try: req = urllib2.Request(url) - # if we have a password, use authentication if password: base64string = base64.encodestring('%s:%s' % (username, password))[:-1] authheader = 'Basic %s' % base64string req.add_header('Authorization', authheader) - self.log(u'Contacting (with auth header) via url: ' + url, logger.DEBUG) + self._log_debug(u'Contacting (with auth header) via url: ' + url) else: - self.log(u'Contacting via url: ' + url, logger.DEBUG) + self._log_debug(u'Contacting via url: ' + url) response = urllib2.urlopen(req) - result = response.read().decode(sickbeard.SYS_ENCODING) response.close() - self.log(u'HTTP response: ' + result.replace('\n', ''), logger.DEBUG) - # could return result response = re.compile('

  • (.+\w)').findall(result) - return 'OK' + self._log_debug(u'HTTP response: ' + result.replace('\n', '')) + return True except (urllib2.URLError, IOError) as e: - self.log(u'Couldn\'t contact Plex at ' + fixStupidEncodings(url) + ' ' + ex(e), logger.WARNING) + self._log_warning(u'Couldn\'t contact Plex at ' + fixStupidEncodings(url) + ' ' + ex(e)) return False - def _notify_pmc(self, message, title='SickGear', host=None, username=None, password=None, force=False): - """Internal wrapper for the notify_snatch and notify_download functions - - Args: - message: Message body of the notice to send - title: Title of the notice to send - host: Plex Media Client(s) host:port - username: Plex username - password: Plex password - force: Used for the Test method to override config safety checks - - Returns: - Returns a list results in the format of host:ip:result - The result will either be 'OK' or False, this is used to be parsed by the calling function. - - """ - - # suppress notifications if the notifier is disabled but the notify options are checked - if not sickbeard.USE_PLEX and not force: - return False - - # fill in omitted parameters - if not host: - host = sickbeard.PLEX_HOST - if not username: - username = sickbeard.PLEX_USERNAME - if not password: - password = sickbeard.PLEX_PASSWORD - - result = '' - for curHost in [x.strip() for x in host.split(',')]: - self.log(u'Sending notification to \'%s\' - %s' % (curHost, message)) - - command = {'command': 'ExecBuiltIn', - 'parameter': 'Notification(%s,%s)' % (title.encode('utf-8'), message.encode('utf-8'))} - notify_result = self._send_to_plex(command, curHost, username, password) - if notify_result: - result += '%s:%s' % (curHost, str(notify_result)) - - return result - -############################################################################## -# Public functions -############################################################################## - - def notify_snatch(self, ep_name): - if sickbeard.PLEX_NOTIFY_ONSNATCH: - self._notify_pmc(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) - - def notify_download(self, ep_name): - if sickbeard.PLEX_NOTIFY_ONDOWNLOAD: - self._notify_pmc(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD]) - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notify_pmc(ep_name + ': ' + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) - - def notify_git_update(self, new_version='??'): - if sickbeard.USE_PLEX: - update_text = common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._notify_pmc(update_text + new_version, title) - - def test_notify(self, host, username, password, server=False): - if server: - return self.update_library(host=host, username=username, password=password, force=False, test=True) - return self._notify_pmc( - 'This is a test notification from SickGear', 'Test', host, username, password, force=True) - @staticmethod def _get_host_list(host='', enable_secure=False): """ @@ -184,7 +95,54 @@ class PLEXNotifier: return host_list - def update_library(self, ep_obj=None, host=None, username=None, password=None, force=True, test=False): + def _notify(self, title, body, host=None, username=None, password=None, **kwargs): + """Internal wrapper for the notify_snatch and notify_download functions + + Args: + title: Title of the notice to send + body: Message body of the notice to send + host: Plex Media Client(s) host:port + username: Plex username + password: Plex password + + Returns: + Returns a test result string for ui output while testing, otherwise True if all tests are a success + """ + host = self._choose(host, sickbeard.PLEX_HOST) + username = self._choose(username, sickbeard.PLEX_USERNAME) + password = self._choose(password, sickbeard.PLEX_PASSWORD) + + command = {'command': 'ExecBuiltIn', + 'parameter': 'Notification(%s,%s)' % (title.encode('utf-8'), body.encode('utf-8'))} + + results = [] + for cur_host in [x.strip() for x in host.split(',')]: + cur_host = urllib.unquote_plus(cur_host) + self._log(u'Sending notification to \'%s\'' % cur_host) + result = self._send_to_plex(command, cur_host, username, password) + results += [self._choose(('%s Plex client ... %s' % (('Successful test notice sent to', + 'Failed test for')[not result], cur_host)), result)] + + return self._choose('
    \n'.join(results), all(results)) + + ############################################################################## + # Public functions + ############################################################################## + + def notify_git_update(self, new_version='??', **kwargs): + # ensure PMS is setup, this is not for when clients are + if sickbeard.PLEX_HOST: + super(PLEXNotifier, self).notify_git_update(new_version, **kwargs) + + def test_update_library(self, host=None, username=None, password=None): + self._testing = True + result = self.update_library(host=urllib.unquote_plus(host), username=username, password=password) + if '
    ' == result: + result += 'Fail: No valid host set to connect with' + return (('Test result for', 'Successful test of')['Fail' not in result] + + ' Plex server(s) ... %s
    \n' % result) + + def update_library(self, ep_obj=None, host=None, username=None, password=None, **kwargs): """Handles updating the Plex Media Server host via HTTP API Plex Media Server currently only supports updating the whole video library and not a specific path. @@ -193,126 +151,120 @@ class PLEXNotifier: Returns None for no issue, else a string of host with connection issues """ + host = self._choose(host, sickbeard.PLEX_SERVER_HOST) + if not host: + msg = u'No Plex Media Server host specified, check your settings' + self._log_debug(msg) + return '%sFail: %s' % (('', '
    ')[self._testing], msg) - if sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY or test: + username = self._choose(username, sickbeard.PLEX_USERNAME) + password = self._choose(password, sickbeard.PLEX_PASSWORD) - if not test: - if not sickbeard.PLEX_SERVER_HOST: - msg = u'No Plex Media Server host specified, check your settings' - self.log(msg, logger.DEBUG) - return '%sFail: %s' % (('', '
    ')[test], msg) + # if username and password were provided, fetch the auth token from plex.tv + token_arg = None + if username and password: - if not host: - host = sickbeard.PLEX_SERVER_HOST - if not username: - username = sickbeard.PLEX_USERNAME - if not password: - password = sickbeard.PLEX_PASSWORD + self._log_debug(u'Fetching plex.tv credentials for user: ' + username) + req = urllib2.Request('https://plex.tv/users/sign_in.xml', data='') + authheader = 'Basic %s' % base64.encodestring('%s:%s' % (username, password))[:-1] + req.add_header('Authorization', authheader) + req.add_header('X-Plex-Device-Name', 'SickGear') + req.add_header('X-Plex-Product', 'SickGear Notifier') + req.add_header('X-Plex-Client-Identifier', '5f48c063eaf379a565ff56c9bb2b401e') + req.add_header('X-Plex-Version', '1.0') + token_arg = False - # if username and password were provided, fetch the auth token from plex.tv - token_arg = None - if username and password: + try: + response = urllib2.urlopen(req) + auth_tree = XmlEtree.parse(response) + token = auth_tree.findall('.//authentication-token')[0].text + token_arg = '?X-Plex-Token=' + token - self.log(u'fetching plex.tv credentials for user: ' + username, logger.DEBUG) - req = urllib2.Request('https://plex.tv/users/sign_in.xml', data='') - authheader = 'Basic %s' % base64.encodestring('%s:%s' % (username, password))[:-1] - req.add_header('Authorization', authheader) - req.add_header('X-Plex-Device-Name', 'SickGear') - req.add_header('X-Plex-Product', 'SickGear Notifier') - req.add_header('X-Plex-Client-Identifier', '5f48c063eaf379a565ff56c9bb2b401e') - req.add_header('X-Plex-Version', '1.0') - token_arg = False + except urllib2.URLError as e: + self._log(u'Error fetching credentials from plex.tv for user %s: %s' % (username, ex(e))) - try: - response = urllib2.urlopen(req) - auth_tree = etree.parse(response) - token = auth_tree.findall('.//authentication-token')[0].text - token_arg = '?X-Plex-Token=' + token + except (ValueError, IndexError) as e: + self._log(u'Error parsing plex.tv response: ' + ex(e)) - except urllib2.URLError as e: - self.log(u'Error fetching credentials from plex.tv for user %s: %s' % (username, ex(e))) + file_location = '' if None is ep_obj else ep_obj.location + host_validate = self._get_host_list(host, all([token_arg])) + hosts_all = {} + hosts_match = {} + hosts_failed = [] + for cur_host in host_validate: + response = sickbeard.helpers.getURL( + '%s/library/sections%s' % (cur_host, token_arg or ''), timeout=10, + mute_connect_err=True, mute_read_timeout=True, mute_connect_timeout=True) + if response: + response = sickbeard.helpers.parse_xml(response) + if not response: + hosts_failed.append(cur_host) + continue - except (ValueError, IndexError) as e: - self.log(u'Error parsing plex.tv response: ' + ex(e)) + sections = response.findall('.//Directory') + if not sections: + self._log(u'Plex Media Server not running on: ' + cur_host) + hosts_failed.append(cur_host) + continue - file_location = '' if None is ep_obj else ep_obj.location - host_validate = self._get_host_list(host, all([token_arg])) - hosts_all = {} - hosts_match = {} - hosts_failed = [] - for cur_host in host_validate: - response = sickbeard.helpers.getURL( - '%s/library/sections%s' % (cur_host, token_arg or ''), timeout=10, - mute_connect_err=True, mute_read_timeout=True, mute_connect_timeout=True) - if response: - response = sickbeard.helpers.parse_xml(response) - if not response: - hosts_failed.append(cur_host) + for section in filter(lambda x: 'show' == x.attrib['type'], sections): + if str(section.attrib['key']) in hosts_all: + continue + keyed_host = [(str(section.attrib['key']), cur_host)] + hosts_all.update(keyed_host) + if not file_location: continue - sections = response.findall('.//Directory') - if not sections: - self.log(u'Plex Media Server not running on: ' + cur_host) - hosts_failed.append(cur_host) - continue + for section_location in section.findall('.//Location'): + section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower()) + section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) + location_path = re.sub(r'[/\\]+', '/', file_location.lower()) + location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) - for section in filter(lambda x: 'show' == x.attrib['type'], sections): - if str(section.attrib['key']) in hosts_all: - continue - keyed_host = [(str(section.attrib['key']), cur_host)] - hosts_all.update(keyed_host) - if not file_location: - continue + if section_path in location_path: + hosts_match.update(keyed_host) + break - for section_location in section.findall('.//Location'): - section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower()) - section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) - location_path = re.sub(r'[/\\]+', '/', file_location.lower()) - location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) - - if section_path in location_path: - hosts_match.update(keyed_host) - break - - if not test: - hosts_try = (hosts_all.copy(), hosts_match.copy())[any(hosts_match)] - host_list = [] - for section_key, cur_host in hosts_try.items(): - refresh_result = None - if force: - refresh_result = sickbeard.helpers.getURL( - '%s/library/sections/%s/refresh%s' % (cur_host, section_key, token_arg or '')) - if (force and '' == refresh_result) or not force: - host_list.append(cur_host) - else: - hosts_failed.append(cur_host) - self.log(u'Error updating library section for Plex Media Server: %s' % cur_host, logger.ERROR) - - if len(hosts_failed) == len(host_validate): - self.log(u'No successful Plex host updated') - return 'Fail no successful Plex host updated: %s' % ', '.join(host for host in hosts_failed) + if not self._testing: + hosts_try = (hosts_all.copy(), hosts_match.copy())[any(hosts_match)] + host_list = [] + for section_key, cur_host in hosts_try.items(): + refresh_result = None + if not self._testing: + refresh_result = sickbeard.helpers.getURL( + '%s/library/sections/%s/refresh%s' % (cur_host, section_key, token_arg or '')) + if (not self._testing and '' == refresh_result) or self._testing: + host_list.append(cur_host) else: - hosts = ', '.join(set(host_list)) - if len(hosts_match): - self.log(u'Hosts updating where TV section paths match the downloaded show: %s' % hosts) - else: - self.log(u'Updating all hosts with TV sections: %s' % hosts) - return '' + hosts_failed.append(cur_host) + self._log_error(u'Error updating library section for Plex Media Server: %s' % cur_host) + + if len(hosts_failed) == len(host_validate): + self._log(u'No successful Plex host updated') + return 'Fail no successful Plex host updated: %s' % ', '.join(host for host in hosts_failed) + else: + hosts = ', '.join(set(host_list)) + if len(hosts_match): + self._log(u'Hosts updating where TV section paths match the downloaded show: %s' % hosts) + else: + self._log(u'Updating all hosts with TV sections: %s' % hosts) + return '' + + hosts = [ + host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_all.values())] + secured = [ + host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_all.values())] + failed = ', '.join([ + host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_failed)]) + failed_secured = ', '.join(filter( + lambda x: x not in hosts, + [host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_failed)])) + return '
    ' + '
    '.join(result for result in [ + ('', 'Fail: username/password when fetching credentials from plex.tv')[False is token_arg], + ('', 'OK (secure connect): %s' % ', '.join(secured))[any(secured)], + ('', 'OK%s: %s' % ((' (legacy connect)', '')[None is token_arg], ', '.join(hosts)))[any(hosts)], + ('', 'Fail (secure connect): %s' % failed_secured)[any(failed_secured)], + ('', 'Fail%s: %s' % ((' (legacy connect)', '')[None is token_arg], failed))[bool(failed)]] if result) - hosts = [ - host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_all.values())] - secured = [ - host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_all.values())] - failed = [ - host.replace('http://', '') for host in filter(lambda x: x.startswith('http:'), hosts_failed)] - failed_secured = ', '.join(filter( - lambda x: x not in hosts, - [host.replace('https://', '') for host in filter(lambda x: x.startswith('https:'), hosts_failed)])) - return '
    ' + '
    '.join(result for result in [ - ('', 'Fail: username/password when fetching credentials from plex.tv')[False is token_arg], - ('', 'OK (secure connect): %s' % ', '.join(secured))[any(secured)], - ('', 'OK%s: %s' % ((' (legacy connect)', '')[None is token_arg], ', '.join(hosts)))[any(hosts)], - ('', 'Fail (secure connect): %s' % failed_secured)[any(failed_secured)], - ('', 'Fail%s: %s' % ((' (legacy connect)', '')[None is token_arg], failed))[any(failed)]] if result) notifier = PLEXNotifier diff --git a/sickbeard/notifiers/prowl.py b/sickbeard/notifiers/prowl.py index 8f3bb114..9318eebc 100644 --- a/sickbeard/notifiers/prowl.py +++ b/sickbeard/notifiers/prowl.py @@ -16,94 +16,50 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from lib.six import moves - import socket +from ssl import SSLError from urllib import urlencode -try: - # this only exists in 2.6 - from ssl import SSLError -except ImportError: - # make a fake one since I don't know what it is supposed to be in 2.5 - class SSLError(Exception): - pass - import sickbeard +from sickbeard.notifiers.generic import Notifier -from sickbeard import logger, common +from lib.six import moves -class ProwlNotifier: - def test_notify(self, prowl_api, prowl_priority): - return self._sendProwl(prowl_api, prowl_priority, event='Test', - message='Testing Prowl settings from SickGear', force=True) +class ProwlNotifier(Notifier): - def notify_snatch(self, ep_name): - if sickbeard.PROWL_NOTIFY_ONSNATCH: - self._sendProwl(prowl_api=None, prowl_priority=None, event=common.notifyStrings[common.NOTIFY_SNATCH], - message=ep_name) + def _notify(self, title, body, prowl_api=None, prowl_priority=None, **kwargs): - def notify_download(self, ep_name): - if sickbeard.PROWL_NOTIFY_ONDOWNLOAD: - self._sendProwl(prowl_api=None, prowl_priority=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD], - message=ep_name) + prowl_api = self._choose(prowl_api, sickbeard.PROWL_API) + prowl_priority = self._choose(prowl_priority, sickbeard.PROWL_PRIORITY) - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD: - self._sendProwl(prowl_api=None, prowl_priority=None, - event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], message=ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_PROWL: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendProwl(prowl_api=None, prowl_priority=None, - event=title, message=update_text + new_version) - - def _sendProwl(self, prowl_api=None, prowl_priority=None, event=None, message=None, force=False): - - if not sickbeard.USE_PROWL and not force: - return False - - if prowl_api == None: - prowl_api = sickbeard.PROWL_API - - if prowl_priority == None: - prowl_priority = sickbeard.PROWL_PRIORITY - - title = 'SickGear' - - logger.log('PROWL: Sending notice with details: event="%s", message="%s", priority=%s, api=%s' % (event, message, prowl_priority, prowl_api), logger.DEBUG) + self._log_debug('Sending notice with details: title="%s", message="%s", priority=%s, api=%s' % ( + title, body, prowl_priority, prowl_api)) http_handler = moves.http_client.HTTPSConnection('api.prowlapp.com') - data = {'apikey': prowl_api, - 'application': title, - 'event': event, - 'description': message.encode('utf-8'), - 'priority': prowl_priority} + data = dict(apikey=prowl_api, application='SickGear', event=title, + description=body.encode('utf-8'), priority=prowl_priority) try: - http_handler.request('POST', - '/publicapi/add', - headers={'Content-type': 'application/x-www-form-urlencoded'}, - body=urlencode(data)) + http_handler.request('POST', '/publicapi/add', + headers={'Content-type': 'application/x-www-form-urlencoded'}, body=urlencode(data)) except (SSLError, moves.http_client.HTTPException, socket.error): - logger.log(u'Prowl notification failed.', logger.ERROR) - return False - response = http_handler.getresponse() - request_status = response.status - - if request_status == 200: - logger.log(u'Prowl notifications sent.', logger.MESSAGE) - return True - elif request_status == 401: - logger.log(u'Prowl authentication failed: %s' % response.reason, logger.ERROR) - return False + result = 'Connection failed' + self._log_error(result) else: - logger.log(u'Prowl notification failed.', logger.ERROR) - return False + response = http_handler.getresponse() + result = None + + if 200 != response.status: + if 401 == response.status: + result = u'Authentication, %s (bad API key?)' % response.reason + else: + result = 'Http response code "%s"' % response.status + + self._log_error(result) + + return self._choose((True, 'Failed to send notification: %s' % result)[bool(result)], not bool(result)) notifier = ProwlNotifier diff --git a/sickbeard/notifiers/pushalot.py b/sickbeard/notifiers/pushalot.py index 16ab0b21..809d1e24 100644 --- a/sickbeard/notifiers/pushalot.py +++ b/sickbeard/notifiers/pushalot.py @@ -17,83 +17,47 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from lib.six import moves - import socket -from urllib import urlencode from ssl import SSLError +from urllib import urlencode import sickbeard -from sickbeard import logger, common +from sickbeard.notifiers.generic import Notifier + +from lib.six import moves -class PushalotNotifier: - def test_notify(self, pushalot_authorizationtoken): - return self._sendPushalot(pushalot_authorizationtoken, event='Test', - message='Testing Pushalot settings from SickGear', force=True) +class PushalotNotifier(Notifier): - def notify_snatch(self, ep_name): - if sickbeard.PUSHALOT_NOTIFY_ONSNATCH: - self._sendPushalot(pushalot_authorizationtoken=None, event=common.notifyStrings[common.NOTIFY_SNATCH], - message=ep_name) + def _notify(self, title, body, pushalot_auth_token=None, **kwargs): - def notify_download(self, ep_name): - if sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD: - self._sendPushalot(pushalot_authorizationtoken=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD], - message=ep_name) + pushalot_auth_token = self._choose(pushalot_auth_token, sickbeard.PUSHALOT_AUTHORIZATIONTOKEN) - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD: - self._sendPushalot(pushalot_authorizationtoken=None, - event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], - message=ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_PUSHALOT: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendPushalot(pushalot_authorizationtoken=None, - event=title, - message=update_text + new_version) - - def _sendPushalot(self, pushalot_authorizationtoken=None, event=None, message=None, force=False): - - if not sickbeard.USE_PUSHALOT and not force: - return False - - if pushalot_authorizationtoken == None: - pushalot_authorizationtoken = sickbeard.PUSHALOT_AUTHORIZATIONTOKEN - - logger.log(u'Pushalot event: ' + event, logger.DEBUG) - logger.log(u'Pushalot message: ' + message, logger.DEBUG) - logger.log(u'Pushalot api: ' + pushalot_authorizationtoken, logger.DEBUG) + self._log_debug(u'Title: %s, Message: %s, API: %s' % (title, body, pushalot_auth_token)) http_handler = moves.http_client.HTTPSConnection('pushalot.com') - data = {'AuthorizationToken': pushalot_authorizationtoken, - 'Title': event.encode('utf-8'), - 'Body': message.encode('utf-8')} - try: - http_handler.request('POST', - '/api/sendmessage', - headers={'Content-type': 'application/x-www-form-urlencoded'}, - body=urlencode(data)) + http_handler.request('POST', '/api/sendmessage', + body=urlencode(dict(Title=title.encode('utf-8'), Body=body.encode('utf-8'), + AuthorizationToken=pushalot_auth_token)), + headers={'Content-type': 'application/x-www-form-urlencoded'}) except (SSLError, moves.http_client.HTTPException, socket.error): - logger.log(u'Pushalot notification failed.', logger.ERROR) - return False - response = http_handler.getresponse() - request_status = response.status - - if request_status == 200: - logger.log(u'Pushalot notifications sent.', logger.DEBUG) - return True - elif request_status == 410: - logger.log(u'Pushalot authentication failed: %s' % response.reason, logger.ERROR) - return False + result = 'Connection failed' + self._log_error(result) else: - logger.log(u'Pushalot notification failed.', logger.ERROR) - return False + response = http_handler.getresponse() + result = None + + if 200 != response.status: + if 410 == response.status: + result = u'Authentication, %s (bad API key?)' % response.reason + else: + result = 'Http response code "%s"' % response.status + + self._log_error(result) + + return self._choose((True, 'Failed to send notification: %s' % result)[bool(result)], not bool(result)) notifier = PushalotNotifier diff --git a/sickbeard/notifiers/pushbullet.py b/sickbeard/notifiers/pushbullet.py index c60dc18c..f4b62e2c 100644 --- a/sickbeard/notifiers/pushbullet.py +++ b/sickbeard/notifiers/pushbullet.py @@ -18,102 +18,63 @@ import base64 import simplejson as json -import sickbeard -from sickbeard import logger -from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT +import sickbeard +from sickbeard.notifiers.generic import Notifier + import requests PUSHAPI_ENDPOINT = 'https://api.pushbullet.com/v2/pushes' DEVICEAPI_ENDPOINT = 'https://api.pushbullet.com/v2/devices' -class PushbulletNotifier: +class PushbulletNotifier(Notifier): - def get_devices(self, accessToken=None): + @staticmethod + def get_devices(access_token=None): # fill in omitted parameters - if not accessToken: - accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN + if not access_token: + access_token = sickbeard.PUSHBULLET_ACCESS_TOKEN # get devices from pushbullet try: - base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1] - headers = {'Authorization': 'Basic %s' % base64string} + base64string = base64.encodestring('%s:%s' % (access_token, ''))[:-1] + headers = dict(Authorization='Basic %s' % base64string) return requests.get(DEVICEAPI_ENDPOINT, headers=headers).text - except Exception as e: - return json.dumps({'error': {'message': 'Error failed to connect: %s' % e}}) + except (StandardError, Exception): + return json.dumps(dict(error=dict(message='Error failed to connect'))) - def _sendPushbullet(self, title, body, accessToken, device_iden): - - # build up the URL and parameters - payload = { - 'type': 'note', - 'title': title, - 'body': body.strip().encode('utf-8'), - 'device_iden': device_iden - } - - # send the request to pushbullet - try: - base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1] - headers = {'Authorization': 'Basic %s' % base64string, 'Content-Type': 'application/json'} - result = requests.post(PUSHAPI_ENDPOINT, headers=headers, data=json.dumps(payload)) - result.raise_for_status() - except Exception as e: - try: - e = result.json()['error']['message'] - except: - pass - logger.log(u'PUSHBULLET: %s' % e, logger.WARNING) - return 'Error sending Pushbullet notification: %s' % e - - logger.log(u'PUSHBULLET: Pushbullet notification succeeded', logger.MESSAGE) - return 'Pushbullet notification succeeded' - - def _notifyPushbullet(self, title, body, accessToken=None, device_iden=None, force=False): + def _notify(self, title, body, access_token=None, device_iden=None, **kwargs): """ Sends a pushbullet notification based on the provided info or SG config title: The title of the notification to send body: The body string to send - accessToken: The access token to grant access + access_token: The access token to grant access device_iden: The iden of a specific target, if none provided send to all devices - force: If True then the notification will be sent even if Pushbullet is disabled in the config """ + access_token = self._choose(access_token, sickbeard.PUSHBULLET_ACCESS_TOKEN) + device_iden = self._choose(device_iden, sickbeard.PUSHBULLET_DEVICE_IDEN) - # suppress notifications if the notifier is disabled but the notify options are checked - if not sickbeard.USE_PUSHBULLET and not force: - return False + # send the request to Pushbullet + result = None + try: + base64string = base64.encodestring('%s:%s' % (access_token, ''))[:-1] + headers = {'Authorization': 'Basic %s' % base64string, 'Content-Type': 'application/json'} + resp = requests.post(PUSHAPI_ENDPOINT, headers=headers, + data=json.dumps(dict( + type='note', title=title, body=body.strip().encode('utf-8'), + device_iden=device_iden))) + resp.raise_for_status() + except (StandardError, Exception): + try: + # noinspection PyUnboundLocalVariable + result = resp.json()['error']['message'] + except (StandardError, Exception): + result = 'no response' + self._log_warning(u'%s' % result) - # fill in omitted parameters - if not accessToken: - accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN - if not device_iden: - device_iden = sickbeard.PUSHBULLET_DEVICE_IDEN + return self._choose((True, 'Failed to send notification: %s' % result)[bool(result)], not bool(result)) - logger.log(u'PUSHBULLET: Sending notice with details: "%s - %s", device_iden: %s' % (title, body, device_iden), logger.DEBUG) - - return self._sendPushbullet(title, body, accessToken, device_iden) - - def notify_snatch(self, ep_name): - if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH: - self._notifyPushbullet(notifyStrings[NOTIFY_SNATCH], ep_name) - - def notify_download(self, ep_name): - if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD: - self._notifyPushbullet(notifyStrings[NOTIFY_DOWNLOAD], ep_name) - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notifyPushbullet(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_PUSHBULLET: - update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT] - title=notifyStrings[NOTIFY_GIT_UPDATE] - self._notifyPushbullet(title, update_text + new_version) - - def test_notify(self, accessToken, device_iden): - return self._notifyPushbullet('Test', 'This is a test notification from SickGear', accessToken, device_iden, force=True) notifier = PushbulletNotifier diff --git a/sickbeard/notifiers/pushover.py b/sickbeard/notifiers/pushover.py index d6ccaa55..29a237de 100644 --- a/sickbeard/notifiers/pushover.py +++ b/sickbeard/notifiers/pushover.py @@ -18,171 +18,114 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import base64 +import socket +import time import urllib import urllib2 -import time -import socket -import base64 import sickbeard -from sickbeard import logger -from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT from sickbeard.exceptions import ex +from sickbeard.notifiers.generic import Notifier API_URL = 'https://api.pushover.net/1/messages.json' DEVICE_URL = 'https://api.pushover.net/1/users/validate.json' -class PushoverNotifier: +class PushoverNotifier(Notifier): - def get_devices(self, userKey=None, apiKey=None): - # fill in omitted parameters - if not userKey: - userKey = sickbeard.PUSHOVER_USERKEY - if not apiKey: - apiKey = sickbeard.PUSHOVER_APIKEY + def get_devices(self, user_key=None, api_key=None): - data = urllib.urlencode({ - 'token': apiKey, - 'user': userKey - }) + user_key = self._choose(user_key, sickbeard.PUSHOVER_USERKEY) + api_key = self._choose(api_key, sickbeard.PUSHOVER_APIKEY) + + data = urllib.urlencode(dict(token=api_key, user=user_key)) # get devices from pushover + result = False try: req = urllib2.Request(DEVICE_URL) handle = urllib2.urlopen(req, data) if handle: result = handle.read() handle.close() - return result - except urllib2.URLError: - return None - except socket.timeout: - return None + except (urllib2.URLError, socket.timeout): + pass - def _sendPushover(self, title, msg, userKey, apiKey, priority, device, sound): + return ('{}', result)[bool(result)] + + def _notify(self, title, body, user_key=None, api_key=None, priority=None, device=None, sound=None, **kwargs): """ Sends a pushover notification to the address provided - - msg: The message to send (unicode) + title: The title of the message - userKey: The pushover user id to send the message to (or to subscribe with) - + msg: The message to send (unicode) + user_key: The pushover user id to send the message to (or to subscribe with) + returns: True if the message succeeded, False otherwise """ - - # fill in omitted parameters - if not userKey: - userKey = sickbeard.PUSHOVER_USERKEY - if not apiKey: - apiKey = sickbeard.PUSHOVER_APIKEY + user_key = self._choose(user_key, sickbeard.PUSHOVER_USERKEY) + api_key = self._choose(api_key, sickbeard.PUSHOVER_APIKEY) + priority = self._choose(priority, sickbeard.PUSHOVER_PRIORITY) + device = self._choose(device, sickbeard.PUSHOVER_DEVICE) + sound = self._choose(sound, sickbeard.PUSHOVER_SOUND) # build up the URL and parameters - msg = msg.strip() - - data = urllib.urlencode({ - 'token': apiKey, - 'title': title, - 'user': userKey, - 'message': msg.encode('utf-8'), - 'priority': priority, - 'device': device, - 'sound': sound, - 'timestamp': int(time.time()) - }) + params = dict(title=title, message=body.strip().encode('utf-8'), user=user_key, timestamp=int(time.time())) + if api_key: + params.update(token=api_key) + if priority: + params.update(priority=priority) + if not device: + params.update(device=device) + if not sound: + params.update(sound=sound) # send the request to pushover + result = None try: req = urllib2.Request(API_URL) - handle = urllib2.urlopen(req, data) + handle = urllib2.urlopen(req, urllib.urlencode(params)) handle.close() except urllib2.URLError as e: # HTTP status 404 if the provided email address isn't a Pushover user. - if e.code == 404: - logger.log(u'PUSHOVER: Username is wrong/not a Pushover email. Pushover will send an email to it', logger.WARNING) - return False + if 404 == e.code: + result = 'Username is wrong/not a Pushover email. Pushover will send an email to it' + self._log_warning(result) - # For HTTP status code 401's, it is because you are passing in either an invalid token, or the user has not added your service. - elif e.code == 401: + # For HTTP status code 401's, it is because you are passing in either an invalid token, + # or the user has not added your service. + elif 401 == e.code: # HTTP status 401 if the user doesn't have the service added - subscribeNote = self._sendPushover(title, msg, userKey) - if subscribeNote: - logger.log(u'PUSHOVER: Subscription sent', logger.DEBUG) - return True + subscribe_note = self._send_pushover(title, body, user_key) + if subscribe_note: + self._log_debug('Subscription sent') + # return True else: - logger.log(u'PUSHOVER: Subscription could not be sent', logger.ERROR) - return False + result = 'Subscription could not be sent' + self._log_error(result) + else: + # If you receive an HTTP status code of 400, it is because you failed to send the proper parameters + if 400 == e.code: + result = 'Wrong data sent to Pushover' - # If you receive an HTTP status code of 400, it is because you failed to send the proper parameters - elif e.code == 400: - logger.log(u'PUSHOVER: Wrong data sent to Pushover', logger.ERROR) - return False + # If you receive a HTTP status code of 429, + # it is because the message limit has been reached (free limit is 7,500) + elif 429 == e.code: + result = 'API message limit reached - try a different API key' - # If you receive a HTTP status code of 429, it is because the message limit has been reached (free limit is 7,500) - elif e.code == 429: - logger.log(u'PUSHOVER: API message limit reached - try a different API key', logger.ERROR) - return False + # If you receive a HTTP status code of 500, service is unavailable + elif 500 == e.code: + result = 'Unable to connect to API, service unavailable' - # If you receive a HTTP status code of 500, service is unavailable - elif e.code == 500: - logger.log(u'PUSHOVER: Unable to connect to API, service unavailable', logger.ERROR) - return False + else: + result = 'Http response code "%s"' % response.status - logger.log(u'PUSHOVER: Notification successful.', logger.MESSAGE) - return True + self._log_error(result) - def _notifyPushover(self, title, message, userKey=None, apiKey=None, priority=None, device=None, sound=None, force=False): - """ - Sends a pushover notification based on the provided info or SG config + return self._choose((True, 'Failed to send notification: %s' % result)[bool(result)], not bool(result)) - title: The title of the notification to send - message: The message string to send - userKey: The userKey to send the notification to - force: Enforce sending, for instance for testing - """ - - # suppress notifications if the notifier is disabled but the notify options are checked - if not sickbeard.USE_PUSHOVER and not force: - logger.log(u'PUSHOVER: Notifications not enabled, skipping this notification', logger.DEBUG) - return False - - # fill in omitted parameters - if not userKey: - userKey = sickbeard.PUSHOVER_USERKEY - if not apiKey: - apiKey = sickbeard.PUSHOVER_APIKEY - if not priority: - priority = sickbeard.PUSHOVER_PRIORITY - if not device: - device = sickbeard.PUSHOVER_DEVICE - if not sound: - sound = sickbeard.PUSHOVER_SOUND - - logger.log(u'PUSHOVER: Sending notice with details: %s - %s, priority: %s, device: %s, sound: %s' % (title, message, priority, device, sound), logger.DEBUG) - - return self._sendPushover(title, message, userKey, apiKey, priority, device, sound) - - def test_notify(self, userKey, apiKey, priority, device, sound): - return self._notifyPushover('Test', 'This is a test notification from SickGear', userKey, apiKey, priority, device, sound, force=True) - - def notify_snatch(self, ep_name): - if sickbeard.PUSHOVER_NOTIFY_ONSNATCH: - self._notifyPushover(notifyStrings[NOTIFY_SNATCH], ep_name) - - def notify_download(self, ep_name): - if sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD: - self._notifyPushover(notifyStrings[NOTIFY_DOWNLOAD], ep_name) - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notifyPushover(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_PUSHOVER: - update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT] - title=notifyStrings[NOTIFY_GIT_UPDATE] - self._notifyPushover(title, update_text + new_version) notifier = PushoverNotifier diff --git a/sickbeard/notifiers/pytivo.py b/sickbeard/notifiers/pytivo.py index 24ce7ad4..681bd8ec 100644 --- a/sickbeard/notifiers/pytivo.py +++ b/sickbeard/notifiers/pytivo.py @@ -17,93 +17,78 @@ # along with SickGear. If not, see . import os -import sickbeard - from urllib import urlencode from urllib2 import Request, urlopen, HTTPError -from sickbeard import logger -from sickbeard.exceptions import ex +import sickbeard +# noinspection PyPep8Naming from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex +from sickbeard.notifiers.generic import BaseNotifier -class pyTivoNotifier: - def notify_snatch(self, ep_name): - pass +class PyTivoNotifier(BaseNotifier): - def notify_download(self, ep_name): - pass - - def notify_subtitle_download(self, ep_name, lang): - pass - - def notify_git_update(self, new_version): - pass - - def update_library(self, ep_obj): - - # Values from config - - if not sickbeard.USE_PYTIVO: - return False + def update_library(self, ep_obj=None, **kwargs): host = sickbeard.PYTIVO_HOST - shareName = sickbeard.PYTIVO_SHARE_NAME + share_name = sickbeard.PYTIVO_SHARE_NAME tsn = sickbeard.PYTIVO_TIVO_NAME # There are two more values required, the container and file. - # + # # container: The share name, show name and season # # file: The file name - # + # # Some slicing and dicing of variables is required to get at these values. # - # There might be better ways to arrive at the values, but this is the best I have been able to + # There might be better ways to arrive at the values, but this is the best I have been able to # come up with. # - # Calculated values - showPath = ep_obj.show.location - showName = ep_obj.show.name - rootShowAndSeason = ek.ek(os.path.dirname, ep_obj.location) - absPath = ep_obj.location + show_path = ep_obj.show.location + show_name = ep_obj.show.name + root_show_and_season = ek.ek(os.path.dirname, ep_obj.location) + abs_path = ep_obj.location # Some show names have colons in them which are illegal in a path location, so strip them out. # (Are there other characters?) - showName = showName.replace(':', '') + show_name = show_name.replace(':', '') - root = showPath.replace(showName, '') - showAndSeason = rootShowAndSeason.replace(root, '') + root = show_path.replace(show_name, '') + show_and_season = root_show_and_season.replace(root, '') - container = shareName + '/' + showAndSeason - file = '/' + absPath.replace(root, '') + container = share_name + '/' + show_and_season + file_path = '/' + abs_path.replace(root, '') # Finally create the url and make request - requestUrl = 'http://' + host + '/TiVoConnect?' + urlencode( - {'Command': 'Push', 'Container': container, 'File': file, 'tsn': tsn}) + request_url = 'http://%s/TiVoConnect?%s' % (host, urlencode( + dict(Command='Push', Container=container, File=file_path, tsn=tsn))) - logger.log(u'pyTivo notification: Requesting ' + requestUrl, logger.DEBUG) + self._log_debug(u'Requesting ' + request_url) - request = Request(requestUrl) + request = Request(request_url) try: - response = urlopen(request) #@UnusedVariable + urlopen(request) + except HTTPError as e: if hasattr(e, 'reason'): - logger.log(u'pyTivo notification: Error, failed to reach a server - ' + e.reason, logger.ERROR) + self._log_error(u'Error, failed to reach a server - ' + e.reason) return False elif hasattr(e, 'code'): - logger.log(u"pyTivo notification: Error, the server couldn't fulfill the request - " + e.code, logger.ERROR) + self._log_error(u'Error, the server couldn\'t fulfill the request - ' + e.code) return False + except Exception as e: - logger.log(u'PYTIVO: Unknown exception: ' + ex(e), logger.ERROR) + self._log_error(u'Unknown exception: ' + ex(e)) return False - else: - logger.log(u'pyTivo notification: Successfully requested transfer of file') - return True + + self._log(u'Successfully requested transfer of file') + return True -notifier = pyTivoNotifier +notifier = PyTivoNotifier diff --git a/sickbeard/notifiers/slack.py b/sickbeard/notifiers/slack.py new file mode 100644 index 00000000..0a9acac4 --- /dev/null +++ b/sickbeard/notifiers/slack.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# Thanks to: mallen86, generica +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import sickbeard +from sickbeard.notifiers.generic import Notifier + + +class SlackNotifier(Notifier): + + def __init__(self): + super(SlackNotifier, self).__init__() + + def _notify(self, title, body, channel='', as_authed=None, bot_name='', icon_url='', access_token='', **kwargs): + + custom = not self._choose(as_authed, sickbeard.SLACK_AS_AUTHED) + resp = sickbeard.helpers.getURL( + url='https://slack.com/api/chat.postMessage', + post_data=dict( + [('text', self._body_only(title, body)), + ('channel', self._choose(channel, sickbeard.SLACK_CHANNEL)), ('as_authed', not custom), + ('token', self._choose(access_token, sickbeard.SLACK_ACCESS_TOKEN))] + + ([], [('username', self._choose(bot_name, sickbeard.SLACK_BOT_NAME) or 'SickGear'), + ('icon_url', self._choose(icon_url, sickbeard.SLACK_ICON_URL) or self._sg_logo_url)])[custom]), + json=True) + + result = resp and resp.get('ok') or 'response: "%s"' % (resp.get('error') or self._choose( + 'bad oath access token?', None)) + if True is not result: + self._log_error('Failed to send message, %s' % result) + + return self._choose(('Successful test notice sent. (Note: %s clients display icon once in a sequence)' + % self.name, 'Error sending notification, %s' % result)[True is not result], result) + + +notifier = SlackNotifier diff --git a/sickbeard/notifiers/synoindex.py b/sickbeard/notifiers/synoindex.py index 56a3148f..a65e38ad 100644 --- a/sickbeard/notifiers/synoindex.py +++ b/sickbeard/notifiers/synoindex.py @@ -16,75 +16,66 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . - - import os import subprocess import sickbeard - -from sickbeard import logger +# noinspection PyPep8Naming from sickbeard import encodingKludge as ek from sickbeard.exceptions import ex +from sickbeard.notifiers.generic import BaseNotifier -class synoIndexNotifier: - def notify_snatch(self, ep_name): - pass - - def notify_download(self, ep_name): - pass - - def notify_subtitle_download(self, ep_name, lang): - pass - - def notify_git_update(self, new_version): - pass +# noinspection PyPep8Naming +class SynoIndexNotifier(BaseNotifier): def moveFolder(self, old_path, new_path): - self.moveObject(old_path, new_path) + self._move_object(old_path, new_path) def moveFile(self, old_file, new_file): - self.moveObject(old_file, new_file) + self._move_object(old_file, new_file) - def moveObject(self, old_path, new_path): - if sickbeard.USE_SYNOINDEX: + def _move_object(self, old_path, new_path): + if self.is_enabled(): synoindex_cmd = ['/usr/syno/bin/synoindex', '-N', ek.ek(os.path.abspath, new_path), ek.ek(os.path.abspath, old_path)] - logger.log(u'Executing command ' + str(synoindex_cmd), logger.DEBUG) - logger.log(u'Absolute path to command: ' + ek.ek(os.path.abspath, synoindex_cmd[0]), logger.DEBUG) + self._log_debug(u'Executing command ' + str(synoindex_cmd)) + self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synoindex_cmd[0])) try: p = subprocess.Popen(synoindex_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) - out, err = p.communicate() #@UnusedVariable - logger.log(u'Script result: ' + str(out), logger.DEBUG) + out, err = p.communicate() + self._log_debug(u'Script result: ' + str(out)) except OSError as e: - logger.log(u'Unable to run synoindex: ' + ex(e), logger.ERROR) + self._log_error(u'Unable to run synoindex: ' + ex(e)) def deleteFolder(self, cur_path): - self.makeObject('-D', cur_path) + self._make_object('-D', cur_path) def addFolder(self, cur_path): - self.makeObject('-A', cur_path) + self._make_object('-A', cur_path) def deleteFile(self, cur_file): - self.makeObject('-d', cur_file) + self._make_object('-d', cur_file) def addFile(self, cur_file): - self.makeObject('-a', cur_file) + self._make_object('-a', cur_file) - def makeObject(self, cmd_arg, cur_path): - if sickbeard.USE_SYNOINDEX: + def _make_object(self, cmd_arg, cur_path): + if self.is_enabled(): synoindex_cmd = ['/usr/syno/bin/synoindex', cmd_arg, ek.ek(os.path.abspath, cur_path)] - logger.log(u'Executing command ' + str(synoindex_cmd), logger.DEBUG) - logger.log(u'Absolute path to command: ' + ek.ek(os.path.abspath, synoindex_cmd[0]), logger.DEBUG) + self._log_debug(u'Executing command ' + str(synoindex_cmd)) + self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synoindex_cmd[0])) try: p = subprocess.Popen(synoindex_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) - out, err = p.communicate() #@UnusedVariable - logger.log(u'Script result: ' + str(out), logger.DEBUG) + out, err = p.communicate() + self._log_debug(u'Script result: ' + str(out)) except OSError as e: - logger.log(u'Unable to run synoindex: ' + ex(e), logger.ERROR) + self._log_error(u'Unable to run synoindex: ' + ex(e)) + + def update_library(self, ep_obj=None, **kwargs): + self.addFile(ep_obj.location) -notifier = synoIndexNotifier +notifier = SynoIndexNotifier diff --git a/sickbeard/notifiers/synologynotifier.py b/sickbeard/notifiers/synologynotifier.py index 34893791..6b17a299 100644 --- a/sickbeard/notifiers/synologynotifier.py +++ b/sickbeard/notifiers/synologynotifier.py @@ -15,49 +15,30 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . - - import os import subprocess import sickbeard - -from sickbeard import logger +# noinspection PyPep8Naming from sickbeard import encodingKludge as ek from sickbeard.exceptions import ex -from sickbeard import common +from sickbeard.notifiers.generic import Notifier -class synologyNotifier: - def notify_snatch(self, ep_name): - if sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH: - self._send_synologyNotifier(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) +class SynologyNotifier(Notifier): - def notify_download(self, ep_name): - if sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD: - self._send_synologyNotifier(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD]) + def _notify(self, title, body, **kwargs): - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD: - self._send_synologyNotifier(ep_name + ': ' + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_SYNOLOGYNOTIFIER: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._send_synologyNotifier(update_text + new_version, title) - - def _send_synologyNotifier(self, message, title): - synodsmnotify_cmd = ['/usr/syno/bin/synodsmnotify', '@administrators', title, message] - logger.log(u'Executing command ' + str(synodsmnotify_cmd)) - logger.log(u'Absolute path to command: ' + ek.ek(os.path.abspath, synodsmnotify_cmd[0]), logger.DEBUG) + synodsmnotify_cmd = ['/usr/syno/bin/synodsmnotify', '@administrators', title, body] + self._log(u'Executing command ' + str(synodsmnotify_cmd)) + self._log_debug(u'Absolute path to command: ' + ek.ek(os.path.abspath, synodsmnotify_cmd[0])) try: p = subprocess.Popen(synodsmnotify_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) - out, err = p.communicate() #@UnusedVariable - logger.log(u'Script result: ' + str(out), logger.DEBUG) + out, err = p.communicate() + self._log_debug(u'Script result: ' + str(out)) except OSError as e: - logger.log(u'Unable to run synodsmnotify: ' + ex(e)) + self._log(u'Unable to run synodsmnotify: ' + ex(e)) -notifier = synologyNotifier +notifier = SynologyNotifier diff --git a/sickbeard/notifiers/trakt.py b/sickbeard/notifiers/trakt.py index 927afff1..c94184ea 100644 --- a/sickbeard/notifiers/trakt.py +++ b/sickbeard/notifiers/trakt.py @@ -16,52 +16,43 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import sickbeard -from sickbeard import logger -from lib.libtrakt import TraktAPI, exceptions import os +import sickbeard +from sickbeard.notifiers.generic import BaseNotifier -class TraktNotifier: - def __init__(self): - pass +from lib.libtrakt import TraktAPI, exceptions + +class TraktNotifier(BaseNotifier): """ A "notifier" for trakt.tv which keeps track of what has and hasn't been added to your library. """ + @classmethod + def is_enabled_library(cls): + if sickbeard.TRAKT_ACCOUNTS: + for tid, locations in sickbeard.TRAKT_UPDATE_COLLECTION.items(): + if tid in sickbeard.TRAKT_ACCOUNTS.keys(): + return True + return False - def notify_snatch(self, ep_name): - pass + def update_library(self, ep_obj=None, **kwargs): - def notify_download(self, ep_name): - pass + self._update_collection(ep_obj) - def notify_subtitle_download(self, ep_name, lang): - pass - - def notify_git_update(self, new_version): - pass - - @staticmethod - def update_collection(ep_obj): + def _update_collection(self, ep_obj): """ Sends a request to trakt indicating that the given episode is part of our collection. :param ep_obj: The TVEpisode object to add to trakt """ - if sickbeard.USE_TRAKT and sickbeard.TRAKT_ACCOUNTS: + if sickbeard.TRAKT_ACCOUNTS: # URL parameters - data = { - 'shows': [ - { - 'title': ep_obj.show.name, - 'year': ep_obj.show.startyear, - 'ids': {}, - } - ] - } + data = dict(shows=[ + dict(title=ep_obj.show.name, year=ep_obj.show.startyear, ids={}) + ]) from sickbeard.indexers.indexer_config import INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_IMDB, INDEXER_TMDB, \ INDEXER_TRAKT @@ -75,12 +66,12 @@ class TraktNotifier: indexer, indexerid = supported_indexer[ep_obj.show.indexer], ep_obj.show.indexerid else: for i in indexer_priorities: - if ep_obj.show.ids.get(i, {'id': 0}).get('id', 0) > 0: + if 0 < ep_obj.show.ids.get(i, {'id': 0}).get('id', 0): indexer, indexerid = supported_indexer[i], ep_obj.show.ids[i]['id'] break - if indexer is None or indexerid is None: - logger.log('Missing trakt supported id, could not add to collection.', logger.WARNING) + if None is indexer or None is indexerid: + self._log_warning('Missing trakt supported id, could not add to collection') return data['shows'][0]['ids'][indexer] = indexerid @@ -101,13 +92,17 @@ class TraktNotifier: warn, msg = False, '' try: resp = TraktAPI().trakt_request('sync/collection', data, send_oauth=tid) - if 'added' in resp and 'episodes' in resp['added'] and 0 < sickbeard.helpers.tryInt(resp['added']['episodes']): + if 'added' in resp and 'episodes' in resp['added'] \ + and 0 < sickbeard.helpers.tryInt(resp['added']['episodes']): msg = 'Added episode to' - elif 'updated' in resp and 'episodes' in resp['updated'] and 0 < sickbeard.helpers.tryInt(resp['updated']['episodes']): + elif 'updated' in resp and 'episodes' in resp['updated'] \ + and 0 < sickbeard.helpers.tryInt(resp['updated']['episodes']): msg = 'Updated episode in' - elif 'existing' in resp and 'episodes' in resp['existing'] and 0 < sickbeard.helpers.tryInt(resp['existing']['episodes']): + elif 'existing' in resp and 'episodes' in resp['existing'] \ + and 0 < sickbeard.helpers.tryInt(resp['existing']['episodes']): msg = 'Episode is already in' - elif 'not_found' in resp and 'episodes' in resp['not_found'] and 0 < sickbeard.helpers.tryInt(resp['not_found']['episodes']): + elif 'not_found' in resp and 'episodes' in resp['not_found'] \ + and 0 < sickbeard.helpers.tryInt(resp['not_found']['episodes']): msg = 'Episode not found on Trakt, not adding to' else: warn, msg = True, 'Could not add episode to' @@ -115,14 +110,9 @@ class TraktNotifier: warn, msg = True, 'Error adding episode to' msg = 'Trakt: %s your %s collection' % (msg, sickbeard.TRAKT_ACCOUNTS[tid].name) if not warn: - logger.log(msg) + self._log(msg) else: - logger.log(msg, logger.WARNING) - - - @staticmethod - def _use_me(): - return sickbeard.USE_TRAKT + self._log_warning(msg) notifier = TraktNotifier diff --git a/sickbeard/notifiers/tweet.py b/sickbeard/notifiers/tweet.py index 7ec628c9..3f5f0296 100644 --- a/sickbeard/notifiers/tweet.py +++ b/sickbeard/notifiers/tweet.py @@ -16,22 +16,18 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +from urlparse import parse_qsl + import sickbeard - -from sickbeard import logger, common from sickbeard.exceptions import ex - -# parse_qsl moved to urlparse module in v2.6 -try: - from urlparse import parse_qsl #@UnusedImport -except: - from cgi import parse_qsl #@Reimport +from sickbeard.notifiers.generic import Notifier import lib.oauth2 as oauth import lib.pythontwitter as twitter -class TwitterNotifier: +class TwitterNotifier(Notifier): + consumer_key = 'vHHtcB6WzpWDG6KYlBMr8g' consumer_secret = 'zMqq5CB3f8cWKiRO2KzWPTlBanYmV0VYxSXZ0Pxds0E' @@ -40,39 +36,19 @@ class TwitterNotifier: AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' - def notify_snatch(self, ep_name): - if sickbeard.TWITTER_NOTIFY_ONSNATCH: - self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + ep_name) + def get_authorization(self): - def notify_download(self, ep_name): - if sickbeard.TWITTER_NOTIFY_ONDOWNLOAD: - self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + ep_name) - - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notifyTwitter(common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD] + ' ' + ep_name + ': ' + lang) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_TWITTER: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._notifyTwitter(title + ' - ' + update_text + new_version) - - def test_notify(self): - return self._notifyTwitter('This is a test notification from SickGear', force=True) - - def _get_authorization(self): - - signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable + # noinspection PyUnusedLocal + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) oauth_client = oauth.Client(oauth_consumer) - logger.log('Requesting temp token from Twitter', logger.DEBUG) + self._log_debug('Requesting temp token from Twitter') resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET') - if resp['status'] != '200': - logger.log('Invalid response from Twitter requesting temp token: %s' % resp['status'], logger.ERROR) + if '200' != resp['status']: + self._log_error('Invalid response from Twitter requesting temp token: %s' % resp['status']) else: request_token = dict(parse_qsl(content)) @@ -81,67 +57,62 @@ class TwitterNotifier: return self.AUTHORIZATION_URL + '?oauth_token=' + request_token['oauth_token'] - def _get_credentials(self, key): - request_token = {} - - request_token['oauth_token'] = sickbeard.TWITTER_USERNAME - request_token['oauth_token_secret'] = sickbeard.TWITTER_PASSWORD - request_token['oauth_callback_confirmed'] = 'true' + def get_credentials(self, key): + request_token = dict(oauth_token=sickbeard.TWITTER_USERNAME, oauth_token_secret=sickbeard.TWITTER_PASSWORD, + oauth_callback_confirmed='true') token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) token.set_verifier(key) - logger.log('Generating and signing request for an access token using key ' + key, logger.DEBUG) + self._log_debug('Generating and signing request for an access token using key ' + key) - signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable + # noinspection PyUnusedLocal + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) - logger.log('oauth_consumer: ' + str(oauth_consumer), logger.DEBUG) + self._log_debug('oauth_consumer: ' + str(oauth_consumer)) oauth_client = oauth.Client(oauth_consumer, token) - logger.log('oauth_client: ' + str(oauth_client), logger.DEBUG) + self._log_debug('oauth_client: ' + str(oauth_client)) resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key) - logger.log('resp, content: ' + str(resp) + ',' + str(content), logger.DEBUG) + self._log_debug('resp, content: ' + str(resp) + ',' + str(content)) access_token = dict(parse_qsl(content)) - logger.log('access_token: ' + str(access_token), logger.DEBUG) + self._log_debug('access_token: ' + str(access_token)) - logger.log('resp[status] = ' + str(resp['status']), logger.DEBUG) - if resp['status'] != '200': - logger.log('The request for a token with did not succeed: ' + str(resp['status']), logger.ERROR) - return False + self._log_debug('resp[status] = ' + str(resp['status'])) + if '200' != resp['status']: + self._log_error('The request for a token with did not succeed: ' + str(resp['status'])) + result = False else: - logger.log('Your Twitter Access Token key: %s' % access_token['oauth_token'], logger.DEBUG) - logger.log('Access Token secret: %s' % access_token['oauth_token_secret'], logger.DEBUG) + self._log_debug('Your Twitter Access Token key: %s' % access_token['oauth_token']) + self._log_debug('Access Token secret: %s' % access_token['oauth_token_secret']) sickbeard.TWITTER_USERNAME = access_token['oauth_token'] sickbeard.TWITTER_PASSWORD = access_token['oauth_token_secret'] - return True + result = True + message = ('Key verification successful', 'Unable to verify key')[not result] + logger.log(u'%s result: %s' % (self.name, message)) + return self._choose(message, result) - def _send_tweet(self, message=None): + def _notify(self, title, body, **kwargs): + + # don't use title with updates or testing, as only one str is used + body = '::'.join(([], [sickbeard.TWITTER_PREFIX])[bool(sickbeard.TWITTER_PREFIX)] + + [body.replace('#: ', ': ') if 'SickGear' in title else body]) username = self.consumer_key password = self.consumer_secret access_token_key = sickbeard.TWITTER_USERNAME access_token_secret = sickbeard.TWITTER_PASSWORD - logger.log(u'Sending tweet: ' + message, logger.DEBUG) - api = twitter.Api(username, password, access_token_key, access_token_secret) try: - api.PostUpdate(message.encode('utf8')) + api.PostUpdate(body.encode('utf8')) except Exception as e: - logger.log(u'Error Sending Tweet: ' + ex(e), logger.ERROR) + self._log_error(u'Error sending Tweet: ' + ex(e)) return False return True - def _notifyTwitter(self, message='', force=False): - prefix = sickbeard.TWITTER_PREFIX - - if not sickbeard.USE_TWITTER and not force: - return False - - return self._send_tweet(prefix + ': ' + message) - notifier = TwitterNotifier diff --git a/sickbeard/notifiers/xbmc.py b/sickbeard/notifiers/xbmc.py index 496a7063..8aa614c8 100644 --- a/sickbeard/notifiers/xbmc.py +++ b/sickbeard/notifiers/xbmc.py @@ -16,32 +16,30 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import urllib -import urllib2 -import socket import base64 -import time - -import sickbeard - -from sickbeard import logger -from sickbeard import common -from sickbeard.exceptions import ex -from sickbeard.encodingKludge import fixStupidEncodings - -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree try: import json except ImportError: from lib import simplejson as json +import socket +import time +import urllib +import urllib2 +import xml.etree.cElementTree as XmlEtree + +import sickbeard +from sickbeard.exceptions import ex +from sickbeard.encodingKludge import fixStupidEncodings +from sickbeard.notifiers.generic import Notifier -class XBMCNotifier: - sb_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon-72x72.png' +class XBMCNotifier(Notifier): + + def __init__(self): + super(XBMCNotifier, self).__init__() + + self.sg_logo_file = 'apple-touch-icon-72x72.png' def _get_xbmc_version(self, host, username, password): """Returns XBMC JSON-RPC API version (odd # = dev, even # = stable) @@ -70,12 +68,12 @@ class XBMCNotifier: """ - # since we need to maintain python 2.5 compatability we can not pass a timeout delay to urllib2 directly (python 2.6+) - # override socket timeout to reduce delay for this call alone + # since we need to maintain python 2.5 compatability we can not pass a timeout delay + # to urllib2 directly (python 2.6+) override socket timeout to reduce delay for this call alone socket.setdefaulttimeout(10) - checkCommand = '{"jsonrpc":"2.0","method":"JSONRPC.Version","id":1}' - result = self._send_to_xbmc_json(checkCommand, host, username, password) + check_command = '{"jsonrpc":"2.0","method":"JSONRPC.Version","id":1}' + result = self._send_to_xbmc_json(check_command, host, username, password) # revert back to default socket timeout socket.setdefaulttimeout(sickbeard.SOCKET_TIMEOUT) @@ -84,113 +82,49 @@ class XBMCNotifier: return result['result']['version'] else: # fallback to legacy HTTPAPI method - testCommand = {'command': 'Help'} - request = self._send_to_xbmc(testCommand, host, username, password) + test_command = {'command': 'Help'} + request = self._send_to_xbmc(test_command, host, username, password) if request: # return a fake version number, so it uses the legacy method return 1 else: return False - def _notify_xbmc(self, message, title='SickGear', host=None, username=None, password=None, force=False): - """Internal wrapper for the notify_snatch and notify_download functions - - Detects JSON-RPC version then branches the logic for either the JSON-RPC or legacy HTTP API methods. - - Args: - message: Message body of the notice to send - title: Title of the notice to send - host: XBMC webserver host:port - username: XBMC webserver username - password: XBMC webserver password - force: Used for the Test method to override config saftey checks - - Returns: - Returns a list results in the format of host:ip:result - The result will either be 'OK' or False, this is used to be parsed by the calling function. - - """ - - # fill in omitted parameters - if not host: - host = sickbeard.XBMC_HOST - if not username: - username = sickbeard.XBMC_USERNAME - if not password: - password = sickbeard.XBMC_PASSWORD - - # suppress notifications if the notifier is disabled but the notify options are checked - if not sickbeard.USE_XBMC and not force: - logger.log('Notification for XBMC not enabled, skipping this notification', logger.DEBUG) - return False - - result = '' - for curHost in [x.strip() for x in host.split(',')]: - logger.log(u'Sending XBMC notification to "' + curHost + '" - ' + message, logger.MESSAGE) - - xbmcapi = self._get_xbmc_version(curHost, username, password) - if xbmcapi: - if (xbmcapi <= 4): - logger.log(u'Detected XBMC version <= 11, using XBMC HTTP API', logger.DEBUG) - command = {'command': 'ExecBuiltIn', - 'parameter': 'Notification(' + title.encode('utf-8') + ',' + message.encode( - 'utf-8') + ')'} - notifyResult = self._send_to_xbmc(command, curHost, username, password) - if notifyResult: - result += curHost + ':' + str(notifyResult) - else: - logger.log(u'Detected XBMC version >= 12, using XBMC JSON API', logger.DEBUG) - command = '{"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"%s","message":"%s", "image": "%s"},"id":1}' % ( - title.encode('utf-8'), message.encode('utf-8'), self.sb_logo_url) - notifyResult = self._send_to_xbmc_json(command, curHost, username, password) - if notifyResult.get('result'): - result += curHost + ':' + notifyResult['result'].decode(sickbeard.SYS_ENCODING) - else: - if sickbeard.XBMC_ALWAYS_ON or force: - logger.log( - u'Failed to detect XBMC version for "' + curHost + '", check configuration and try again.', - logger.ERROR) - result += curHost + ':False' - - return result - - def _send_update_library(self, host, showName=None): + def _send_update_library(self, host, show_name=None): """Internal wrapper for the update library function to branch the logic for JSON-RPC or legacy HTTP API - Checks the XBMC API version to branch the logic to call either the legacy HTTP API or the newer JSON-RPC over HTTP methods. + Checks the XBMC API version to branch the logic + to call either the legacy HTTP API or the newer JSON-RPC over HTTP methods. Args: host: XBMC webserver host:port - showName: Name of a TV show to specifically target the library update for + show_name: Name of a TV show to specifically target the library update for Returns: Returns True or False, if the update was successful """ - logger.log(u'Sending request to update library for XBMC host: "' + host + '"', logger.MESSAGE) + self._log(u'Sending request to update library for host: "%s"' % host) xbmcapi = self._get_xbmc_version(host, sickbeard.XBMC_USERNAME, sickbeard.XBMC_PASSWORD) if xbmcapi: - if (xbmcapi <= 4): + if 4 >= xbmcapi: # try to update for just the show, if it fails, do full update if enabled - if not self._update_library(host, showName) and sickbeard.XBMC_UPDATE_FULL: - logger.log(u'Single show update failed, falling back to full update', logger.WARNING) - return self._update_library(host) + if not self._update_library_http(host, show_name) and sickbeard.XBMC_UPDATE_FULL: + self._log_warning(u'Single show update failed, falling back to full update') + return self._update_library_http(host) else: return True else: # try to update for just the show, if it fails, do full update if enabled - if not self._update_library_json(host, showName) and sickbeard.XBMC_UPDATE_FULL: - logger.log(u'Single show update failed, falling back to full update', logger.WARNING) + if not self._update_library_json(host, show_name) and sickbeard.XBMC_UPDATE_FULL: + self._log_warning(u'Single show update failed, falling back to full update') return self._update_library_json(host) else: return True - else: - logger.log(u'Failed to detect XBMC version for "' + host + '", check configuration and try again.', - logger.DEBUG) - return False + self._log_debug(u'Failed to detect version for "%s", check configuration and try again' % host) return False # ############################################################################# @@ -210,23 +144,19 @@ class XBMCNotifier: Returns response.result for successful commands or False if there was an error """ - - # fill in omitted parameters - if not username: - username = sickbeard.XBMC_USERNAME - if not password: - password = sickbeard.XBMC_PASSWORD - if not host: - logger.log(u'No XBMC host passed, aborting update', logger.DEBUG) + self._log_debug(u'No host passed, aborting update') return False + username = self._choose(username, sickbeard.XBMC_USERNAME) + password = self._choose(password, sickbeard.XBMC_PASSWORD) + for key in command: if type(command[key]) == unicode: command[key] = command[key].encode('utf-8') enc_command = urllib.urlencode(command) - logger.log(u'XBMC encoded API command: ' + enc_command, logger.DEBUG) + self._log_debug(u'Encoded API command: ' + enc_command) url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, enc_command) try: @@ -236,23 +166,22 @@ class XBMCNotifier: base64string = base64.encodestring('%s:%s' % (username, password))[:-1] authheader = 'Basic %s' % base64string req.add_header('Authorization', authheader) - logger.log(u'Contacting XBMC (with auth header) via url: ' + fixStupidEncodings(url), logger.DEBUG) + self._log_debug(u'Contacting (with auth header) via url: ' + fixStupidEncodings(url)) else: - logger.log(u'Contacting XBMC via url: ' + fixStupidEncodings(url), logger.DEBUG) + self._log_debug(u'Contacting via url: ' + fixStupidEncodings(url)) response = urllib2.urlopen(req) result = response.read().decode(sickbeard.SYS_ENCODING) response.close() - logger.log(u'XBMC HTTP response: ' + result.replace('\n', ''), logger.DEBUG) + self._log_debug(u'HTTP response: ' + result.replace('\n', '')) return result except (urllib2.URLError, IOError) as e: - logger.log(u"Warning: Couldn't contact XBMC HTTP at " + fixStupidEncodings(url) + ' ' + ex(e), - logger.WARNING) + self._log_warning(u'Couldn\'t contact HTTP at %s %s' % (fixStupidEncodings(url), ex(e))) return False - def _update_library(self, host=None, showName=None): + def _update_library_http(self, host=None, show_name=None): """Handles updating XBMC host via HTTP API Attempts to update the XBMC video library for a specific tv show if passed, @@ -260,7 +189,7 @@ class XBMCNotifier: Args: host: XBMC webserver host:port - showName: Name of a TV show to specifically target the library update for + show_name: Name of a TV show to specifically target the library update for Returns: Returns True or False @@ -268,73 +197,76 @@ class XBMCNotifier: """ if not host: - logger.log(u'No XBMC host passed, aborting update', logger.DEBUG) + self._log_debug(u'No host passed, aborting update') return False - logger.log(u'Updating XMBC library via HTTP method for host: ' + host, logger.DEBUG) + self._log_debug(u'Updating XMBC library via HTTP method for host: ' + host) # if we're doing per-show - if showName: - logger.log(u'Updating library in XBMC via HTTP method for show ' + showName, logger.DEBUG) + if show_name: + self._log_debug(u'Updating library via HTTP method for show ' + show_name) - pathSql = 'select path.strPath from path, tvshow, tvshowlinkpath where ' \ - 'tvshow.c00 = "%s" and tvshowlinkpath.idShow = tvshow.idShow ' \ - 'and tvshowlinkpath.idPath = path.idPath' % (showName) + # noinspection SqlResolve + path_sql = 'select path.strPath' \ + ' from path, tvshow, tvshowlinkpath' \ + ' where tvshow.c00 = "%s"' \ + ' and tvshowlinkpath.idShow = tvshow.idShow' \ + ' and tvshowlinkpath.idPath = path.idPath' % show_name # use this to get xml back for the path lookups - xmlCommand = { - 'command': 'SetResponseFormat(webheader;false;webfooter;false;header;;footer;;opentag;;closetag;;closefinaltag;false)'} + xml_command = dict(command='SetResponseFormat(webheader;false;webfooter;false;header;;footer;;' + 'opentag;;closetag;;closefinaltag;false)') # sql used to grab path(s) - sqlCommand = {'command': 'QueryVideoDatabase(%s)' % (pathSql)} + sql_command = dict(command='QueryVideoDatabase(%s)' % path_sql) # set output back to default - resetCommand = {'command': 'SetResponseFormat()'} + reset_command = dict(command='SetResponseFormat()') # set xml response format, if this fails then don't bother with the rest - request = self._send_to_xbmc(xmlCommand, host) + request = self._send_to_xbmc(xml_command, host) if not request: return False - sqlXML = self._send_to_xbmc(sqlCommand, host) - request = self._send_to_xbmc(resetCommand, host) + sql_xml = self._send_to_xbmc(sql_command, host) + self._send_to_xbmc(reset_command, host) - if not sqlXML: - logger.log(u'Invalid response for ' + showName + ' on ' + host, logger.DEBUG) + if not sql_xml: + self._log_debug(u'Invalid response for ' + show_name + ' on ' + host) return False - encSqlXML = urllib.quote(sqlXML, ':\\/<>') + enc_sql_xml = urllib.quote(sql_xml, ':\\/<>') try: - et = etree.fromstring(encSqlXML) + et = XmlEtree.fromstring(enc_sql_xml) except SyntaxError as e: - logger.log(u'Unable to parse XML returned from XBMC: ' + ex(e), logger.ERROR) + self._log_error(u'Unable to parse XML response: ' + ex(e)) return False paths = et.findall('.//field') if not paths: - logger.log(u'No valid paths found for ' + showName + ' on ' + host, logger.DEBUG) + self._log_debug(u'No valid paths found for ' + show_name + ' on ' + host) return False for path in paths: # we do not need it double-encoded, gawd this is dumb - unEncPath = urllib.unquote(path.text).decode(sickbeard.SYS_ENCODING) - logger.log(u'XBMC Updating ' + showName + ' on ' + host + ' at ' + unEncPath, logger.DEBUG) - updateCommand = {'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video, %s)' % (unEncPath)} - request = self._send_to_xbmc(updateCommand, host) + un_enc_path = urllib.unquote(path.text).decode(sickbeard.SYS_ENCODING) + self._log_debug(u'Updating ' + show_name + ' on ' + host + ' at ' + un_enc_path) + update_command = dict(command='ExecBuiltIn', parameter='XBMC.updatelibrary(video, %s)' % un_enc_path) + request = self._send_to_xbmc(update_command, host) if not request: - logger.log(u'Update of show directory failed on ' + showName + ' on ' + host + ' at ' + unEncPath, - logger.ERROR) + self._log_error(u'Update of show directory failed on ' + show_name + + ' on ' + host + ' at ' + un_enc_path) return False # sleep for a few seconds just to be sure xbmc has a chance to finish each directory if len(paths) > 1: time.sleep(5) # do a full update if requested else: - logger.log(u'Doing Full Library XBMC update on host: ' + host, logger.MESSAGE) - updateCommand = {'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'} - request = self._send_to_xbmc(updateCommand, host) + self._log(u'Doing full library update on host: ' + host) + update_command = {'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'} + request = self._send_to_xbmc(update_command, host) if not request: - logger.log(u'XBMC Full Library update failed on: ' + host, logger.ERROR) + self._log_error(u'Full Library update failed on: ' + host) return False return True @@ -356,21 +288,17 @@ class XBMCNotifier: Returns response.result for successful commands or False if there was an error """ - - # fill in omitted parameters - if not username: - username = sickbeard.XBMC_USERNAME - if not password: - password = sickbeard.XBMC_PASSWORD - if not host: - logger.log(u'No XBMC host passed, aborting update', logger.DEBUG) + self._log_debug(u'No host passed, aborting update') return False - command = command.encode('utf-8') - logger.log(u'XBMC JSON command: ' + command, logger.DEBUG) + username = self._choose(username, sickbeard.XBMC_USERNAME) + password = self._choose(password, sickbeard.XBMC_PASSWORD) - url = 'http://%s/jsonrpc' % (host) + command = command.encode('utf-8') + self._log_debug(u'JSON command: ' + command) + + url = 'http://%s/jsonrpc' % host try: req = urllib2.Request(url, command) req.add_header('Content-type', 'application/json') @@ -379,33 +307,31 @@ class XBMCNotifier: base64string = base64.encodestring('%s:%s' % (username, password))[:-1] authheader = 'Basic %s' % base64string req.add_header('Authorization', authheader) - logger.log(u'Contacting XBMC (with auth header) via url: ' + fixStupidEncodings(url), logger.DEBUG) + self._log_debug(u'Contacting (with auth header) via url: ' + fixStupidEncodings(url)) else: - logger.log(u'Contacting XBMC via url: ' + fixStupidEncodings(url), logger.DEBUG) + self._log_debug(u'Contacting via url: ' + fixStupidEncodings(url)) try: response = urllib2.urlopen(req) except urllib2.URLError as e: - logger.log(u'Error while trying to retrieve XBMC API version for ' + host + ': ' + ex(e), - logger.WARNING) + self._log_warning(u'Error while trying to retrieve API version for "%s": %s' % (host, ex(e))) return False # parse the json result try: result = json.load(response) response.close() - logger.log(u'XBMC JSON response: ' + str(result), logger.DEBUG) + self._log_debug(u'JSON response: ' + str(result)) return result # need to return response for parsing - except ValueError as e: - logger.log(u'Unable to decode JSON: ' + response, logger.WARNING) + except ValueError: + self._log_warning(u'Unable to decode JSON: ' + response) return False except IOError as e: - logger.log(u"Warning: Couldn't contact XBMC JSON API at " + fixStupidEncodings(url) + ' ' + ex(e), - logger.WARNING) + self._log_warning(u'Couldn\'t contact JSON API at ' + fixStupidEncodings(url) + ' ' + ex(e)) return False - def _update_library_json(self, host=None, showName=None): + def _update_library_json(self, host=None, show_name=None): """Handles updating XBMC host via HTTP JSON-RPC Attempts to update the XBMC video library for a specific tv show if passed, @@ -413,7 +339,7 @@ class XBMCNotifier: Args: host: XBMC webserver host:port - showName: Name of a TV show to specifically target the library update for + show_name: Name of a TV show to specifically target the library update for Returns: Returns True or False @@ -421,28 +347,28 @@ class XBMCNotifier: """ if not host: - logger.log(u'No XBMC host passed, aborting update', logger.DEBUG) + self._log_debug(u'No host passed, aborting update') return False - logger.log(u'Updating XMBC library via JSON method for host: ' + host, logger.MESSAGE) + self._log(u'Updating XMBC library via JSON method for host: ' + host) # if we're doing per-show - if showName: + if show_name: tvshowid = -1 - logger.log(u'Updating library in XBMC via JSON method for show ' + showName, logger.DEBUG) + self._log_debug(u'Updating library via JSON method for show ' + show_name) # get tvshowid by showName - showsCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShows","id":1}' - showsResponse = self._send_to_xbmc_json(showsCommand, host) + shows_command = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShows","id":1}' + shows_response = self._send_to_xbmc_json(shows_command, host) - if showsResponse and 'result' in showsResponse and 'tvshows' in showsResponse['result']: - shows = showsResponse['result']['tvshows'] + if shows_response and 'result' in shows_response and 'tvshows' in shows_response['result']: + shows = shows_response['result']['tvshows'] else: - logger.log(u'XBMC: No tvshows in XBMC TV show list', logger.DEBUG) + self._log_debug(u'No tvshows in TV show list') return False for show in shows: - if (show['label'] == showName): + if show['label'] == show_name: tvshowid = show['tvshowid'] break # exit out of loop otherwise the label and showname will not match up @@ -450,121 +376,145 @@ class XBMCNotifier: del shows # we didn't find the show (exact match), thus revert to just doing a full update if enabled - if (tvshowid == -1): - logger.log(u'Exact show name not matched in XBMC TV show list', logger.DEBUG) + if -1 == tvshowid: + self._log_debug(u'Exact show name not matched in TV show list') return False # lookup tv-show path - pathCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShowDetails","params":{"tvshowid":%d, "properties": ["file"]},"id":1}' % ( - tvshowid) - pathResponse = self._send_to_xbmc_json(pathCommand, host) + path_command = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShowDetails",' \ + '"params":{"tvshowid":%d, "properties": ["file"]},"id":1}' % tvshowid + path_response = self._send_to_xbmc_json(path_command, host) - path = pathResponse['result']['tvshowdetails']['file'] - logger.log(u'Received Show: ' + showName + ' with ID: ' + str(tvshowid) + ' Path: ' + path, - logger.DEBUG) + path = path_response['result']['tvshowdetails']['file'] + self._log_debug(u'Received Show: ' + show_name + ' with ID: ' + str(tvshowid) + ' Path: ' + path) - if (len(path) < 1): - logger.log(u'No valid path found for ' + showName + ' with ID: ' + str(tvshowid) + ' on ' + host, - logger.WARNING) + if 1 > len(path): + self._log_warning(u'No valid path found for ' + show_name + ' with ID: ' + + str(tvshowid) + ' on ' + host) return False - logger.log(u'XBMC Updating ' + showName + ' on ' + host + ' at ' + path, logger.DEBUG) - updateCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","params":{"directory":%s},"id":1}' % ( + self._log_debug(u'Updating ' + show_name + ' on ' + host + ' at ' + path) + update_command = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","params":{"directory":%s},"id":1}' % ( json.dumps(path)) - request = self._send_to_xbmc_json(updateCommand, host) + request = self._send_to_xbmc_json(update_command, host) if not request: - logger.log(u'Update of show directory failed on ' + showName + ' on ' + host + ' at ' + path, - logger.ERROR) + self._log_error(u'Update of show directory failed on ' + show_name + ' on ' + host + ' at ' + path) return False # catch if there was an error in the returned request + # noinspection PyTypeChecker for r in request: if 'error' in r: - logger.log( - u'Error while attempting to update show directory for ' + showName + ' on ' + host + ' at ' + path, - logger.ERROR) + self._log_error( + u'Error while attempting to update show directory for ' + show_name + + ' on ' + host + ' at ' + path) return False # do a full update if requested else: - logger.log(u'Doing Full Library XBMC update on host: ' + host, logger.MESSAGE) - updateCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","id":1}' - request = self._send_to_xbmc_json(updateCommand, host, sickbeard.XBMC_USERNAME, sickbeard.XBMC_PASSWORD) + self._log(u'Doing Full Library update on host: ' + host) + update_command = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","id":1}' + request = self._send_to_xbmc_json(update_command, host, sickbeard.XBMC_USERNAME, sickbeard.XBMC_PASSWORD) if not request: - logger.log(u'XBMC Full Library update failed on: ' + host, logger.ERROR) + self._log_error(u'Full Library update failed on: ' + host) return False return True - ############################################################################## - # Public functions which will call the JSON or Legacy HTTP API methods - ############################################################################## + def _notify(self, title, body, hosts=None, username=None, password=None, **kwargs): + """Internal wrapper for the notify_snatch and notify_download functions - def notify_snatch(self, ep_name): - if sickbeard.XBMC_NOTIFY_ONSNATCH: - self._notify_xbmc(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) + Detects JSON-RPC version then branches the logic for either the JSON-RPC or legacy HTTP API methods. - def notify_download(self, ep_name): - if sickbeard.XBMC_NOTIFY_ONDOWNLOAD: - self._notify_xbmc(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD]) + Args: + title: Title of the notice to send + body: Message body of the notice to send + hosts: XBMC webserver host:port + username: XBMC webserver username + password: XBMC webserver password - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD: - self._notify_xbmc(ep_name + ': ' + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) - - def notify_git_update(self, new_version = '??'): - if sickbeard.USE_XBMC: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._notify_xbmc(update_text + new_version, title) + Returns: + Returns a list results in the format of host:ip:result + The result will either be 'OK' or False, this is used to be parsed by the calling function. - def test_notify(self, host, username, password): - return self._notify_xbmc('Testing XBMC notifications from SickGear', 'Test Notification', host, username, - password, force=True) + """ + hosts = self._choose(hosts, sickbeard.XBMC_HOST) + username = self._choose(username, sickbeard.XBMC_USERNAME) + password = self._choose(password, sickbeard.XBMC_PASSWORD) - def update_library(self, showName=None): + success = False + result = [] + for cur_host in [x.strip() for x in hosts.split(',')]: + cur_host = urllib.unquote_plus(cur_host) + + self._log(u'Sending notification to "%s"' % cur_host) + + xbmcapi = self._get_xbmc_version(cur_host, username, password) + if xbmcapi: + if 4 >= xbmcapi: + self._log_debug(u'Detected version <= 11, using HTTP API') + command = dict(command='ExecBuiltIn', + parameter='Notification(' + title.encode('utf-8') + ',' + body.encode('utf-8') + ')') + notify_result = self._send_to_xbmc(command, cur_host, username, password) + if notify_result: + result += [cur_host + ':' + str(notify_result)] + success |= 'OK' in notify_result or success + else: + self._log_debug(u'Detected version >= 12, using JSON API') + command = '{"jsonrpc":"2.0","method":"GUI.ShowNotification",' \ + '"params":{"title":"%s","message":"%s", "image": "%s"},"id":1}' % \ + (title.encode('utf-8'), body.encode('utf-8'), self._sg_logo_url) + notify_result = self._send_to_xbmc_json(command, cur_host, username, password) + if notify_result.get('result'): + result += [cur_host + ':' + notify_result['result'].decode(sickbeard.SYS_ENCODING)] + success |= 'OK' in notify_result or success + else: + if sickbeard.XBMC_ALWAYS_ON or self._testing: + self._log_error(u'Failed to detect version for "%s", check configuration and try again' % cur_host) + result += [cur_host + ':No response'] + success = False + + return self._choose(('Success, all hosts tested', '
    \n'.join(result))[not bool(success)], bool(success)) + + def update_library(self, show_name=None, **kwargs): """Public wrapper for the update library functions to branch the logic for JSON-RPC or legacy HTTP API - Checks the XBMC API version to branch the logic to call either the legacy HTTP API or the newer JSON-RPC over HTTP methods. - Do the ability of accepting a list of hosts deliminated by comma, only one host is updated, the first to respond with success. + Checks the XBMC API version to branch the logic to call either the legacy HTTP API + or the newer JSON-RPC over HTTP methods. + Do the ability of accepting a list of hosts delimited by comma, only one host is updated, + the first to respond with success. This is a workaround for SQL backend users as updating multiple clients causes duplicate entries. Future plan is to revist how we store the host/ip/username/pw/options so that it may be more flexible. Args: - showName: Name of a TV show to specifically target the library update for + show_name: Name of a TV show to specifically target the library update for Returns: Returns True or False """ + if not sickbeard.XBMC_HOST: + self._log_debug(u'No hosts specified, check your settings') + return False - if sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY: - if not sickbeard.XBMC_HOST: - logger.log(u'No XBMC hosts specified, check your settings', logger.DEBUG) - return False - - # either update each host, or only attempt to update until one successful result - result = 0 - for host in [x.strip() for x in sickbeard.XBMC_HOST.split(',')]: - if self._send_update_library(host, showName): - if sickbeard.XBMC_UPDATE_ONLYFIRST: - logger.log(u'Successfully updated "' + host + '", stopped sending update library commands.', - logger.DEBUG) - return True - else: - if sickbeard.XBMC_ALWAYS_ON: - logger.log( - u'Failed to detect XBMC version for "' + host + '", check configuration and try again.', - logger.ERROR) - result = result + 1 - - # needed for the 'update xbmc' submenu command - # as it only cares of the final result vs the individual ones - if result == 0: - return True + # either update each host, or only attempt to update until one successful result + result = 0 + for host in [x.strip() for x in sickbeard.XBMC_HOST.split(',')]: + if self._send_update_library(host, show_name): + if sickbeard.XBMC_UPDATE_ONLYFIRST: + self._log_debug(u'Successfully updated "%s", stopped sending update library commands' % host) + return True else: - return False + if sickbeard.XBMC_ALWAYS_ON: + self._log_error(u'Failed to detect version for "%s", check configuration and try again' % host) + result = result + 1 + + # needed for the 'update xbmc' submenu command + # as it only cares of the final result vs the individual ones + if not 0 != result: + return False + return True notifier = XBMCNotifier diff --git a/sickbeard/nzbSplitter.py b/sickbeard/nzbSplitter.py index 14c467cf..c79e8788 100644 --- a/sickbeard/nzbSplitter.py +++ b/sickbeard/nzbSplitter.py @@ -18,9 +18,16 @@ from __future__ import with_statement -import xml.etree.cElementTree as etree -import xml.etree +try: + from lxml import etree +except ImportError: + try: + import xml.etree.cElementTree as etree + except ImportError: + import xml.etree.ElementTree as etree + import re +import os from name_parser.parser import NameParser, InvalidNameException, InvalidShowException @@ -28,6 +35,32 @@ from sickbeard import logger, classes, helpers from sickbeard.common import Quality from sickbeard import encodingKludge as ek from sickbeard.exceptions import ex +import sickbeard + + +SUBJECT_FN_MATCHER = re.compile(r'"([^"]*)"') +RE_NORMAL_NAME = re.compile(r'\.\w{1,5}$') + + +def platform_encode(p): + """ Return Unicode name, if not already Unicode, decode with UTF-8 or latin1 """ + if isinstance(p, str): + try: + return p.decode('utf-8') + except: + return p.decode(sickbeard.SYS_ENCODING, errors='replace').replace('?', '!') + else: + return p + + +def name_extractor(subject): + """ Try to extract a file name from a subject line, return `subject` if in doubt """ + result = subject + for name in re.findall(SUBJECT_FN_MATCHER, subject): + name = name.strip(' "') + if name and RE_NORMAL_NAME.search(name): + result = name + return platform_encode(result) def getSeasonNZBs(name, urlData, season): @@ -35,29 +68,31 @@ def getSeasonNZBs(name, urlData, season): showXML = etree.ElementTree(etree.XML(urlData)) except SyntaxError: logger.log(u"Unable to parse the XML of " + name + ", not splitting it", logger.ERROR) - return ({}, '') + return {}, '' filename = name.replace(".nzb", "") nzbElement = showXML.getroot() - regex = '([\w\._\ ]+)[\. ]S%02d[\. ]([\w\._\-\ ]+)[\- ]([\w_\-\ ]+?)' % season + regex = '([\w\._\ ]+)[\._ ]S%02d[\._ ]([\w\._\-\ ]+)' % season sceneNameMatch = re.search(regex, filename, re.I) if sceneNameMatch: - showName, qualitySection, groupName = sceneNameMatch.groups() # @UnusedVariable + showName, qualitySection = sceneNameMatch.groups() # @UnusedVariable else: - logger.log(u"Unable to parse " + name + " into a scene name. If it's a valid one, log a bug.", logger.ERROR) - return ({}, '') + logger.log("%s - Not a valid season pack scene name. If it's a valid one, log a bug." % name, logger.ERROR) + return {}, '' - regex = '(' + re.escape(showName) + '\.S%02d(?:[E0-9]+)\.[\w\._]+\-\w+' % season + ')' + regex = '(' + re.escape(showName) + '[\._]S%02d(?:[E0-9]+)\.[\w\._]+' % season + ')' regex = regex.replace(' ', '.') epFiles = {} xmlns = None for curFile in nzbElement.getchildren(): - xmlnsMatch = re.match("\{(http:\/\/[A-Za-z0-9_\.\/]+\/nzb)\}file", curFile.tag) + if not isinstance(curFile.tag, basestring): + continue + xmlnsMatch = re.match("\{(https?:\/\/[A-Za-z0-9_\.\/]+\/nzb)\}file", curFile.tag) if not xmlnsMatch: continue else: @@ -67,12 +102,22 @@ def getSeasonNZBs(name, urlData, season): #print curFile.get("subject"), "doesn't match", regex continue curEp = match.group(1) + fn = name_extractor(curFile.get('subject', '')) + if curEp == re.sub(r'\+\d+\.par2$', '', fn, flags=re.I): + bn, ext = ek.ek(os.path.splitext, fn) + curEp = re.sub(r'\.(part\d+|vol\d+(\+\d+)?)$', '', bn, flags=re.I) + bn, ext = ek.ek(os.path.splitext, curEp) + if isinstance(ext, basestring) \ + and re.search(r'^\.(nzb|r\d{2}|rar|7z|zip|par2|vol\d+|nfo|srt|txt|bat|sh|mkv|mp4|avi|wmv)$', ext, + flags=re.I): + logger.log('Unable to split %s into episode nzb\'s' % name, logger.WARNING) + return {}, '' if curEp not in epFiles: epFiles[curEp] = [curFile] else: epFiles[curEp].append(curFile) - return (epFiles, xmlns) + return epFiles, xmlns def createNZBString(fileElements, xmlns): @@ -83,7 +128,7 @@ def createNZBString(fileElements, xmlns): for curFile in fileElements: rootElement.append(stripNS(curFile, xmlns)) - return xml.etree.ElementTree.tostring(rootElement, 'utf-8') + return etree.tostring(rootElement, encoding='utf-8') def saveNZB(nzbName, nzbString): diff --git a/sickbeard/nzbget.py b/sickbeard/nzbget.py index 2fc4cced..77a6af97 100644 --- a/sickbeard/nzbget.py +++ b/sickbeard/nzbget.py @@ -18,7 +18,6 @@ import datetime import re import sickbeard -import config from lib.six import moves from base64 import standard_b64encode from common import Quality @@ -64,7 +63,7 @@ def test_nzbget(host, use_https, username, password): return result, msg, rpc_client -def send_nzb(nzb, proper=False): +def send_nzb(nzb): result = False add_to_top = False nzbget_prio = 0 @@ -80,7 +79,7 @@ def send_nzb(nzb, proper=False): # if it aired recently make it high priority and generate DupeKey/Score for curEp in nzb.episodes: if '' == dupekey: - dupekey = "SickGear-%s%s" % ( + dupekey = 'SickGear-%s%s' % ( sickbeard.indexerApi(curEp.show.indexer).config.get('dupekey', ''), curEp.show.indexerid) dupekey += '-%s.%s' % (curEp.season, curEp.episode) @@ -90,12 +89,14 @@ def send_nzb(nzb, proper=False): if Quality.UNKNOWN != nzb.quality: dupescore = nzb.quality * 100 - if proper: - dupescore += 10 + + dupescore += (0, 9 + nzb.properlevel)[0 < nzb.properlevel] nzbcontent64 = None if 'nzbdata' == nzb.resultType: - data = nzb.extraInfo[0] + data = nzb.get_data() + if not data: + return False nzbcontent64 = standard_b64encode(data) elif 'Anizb' == nzb.provider.name and 'nzb' == nzb.resultType: gen_provider = GenericProvider('') @@ -153,7 +154,7 @@ def send_nzb(nzb, proper=False): result = True else: logger.log(u'NZBget could not add %s to the queue' % ('%s.nzb' % nzb.name), logger.ERROR) - except: + except(StandardError, Exception): logger.log(u'Connect Error to NZBget: could not add %s to the queue' % ('%s.nzb' % nzb.name), logger.ERROR) return result diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index 7127b0bb..95627956 100644 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -247,7 +247,7 @@ class PostProcessor(object): self._log(u'Deleted file ' + cur_file, logger.DEBUG) # do the library update for synoindex - notifiers.synoindex_notifier.deleteFile(cur_file) + notifiers.NotifierFactory().get('SYNOINDEX').deleteFile(cur_file) def _combined_file_operation(self, file_path, new_path, new_base_name, associated_files=False, action=None, subtitles=False, action_tmpl=None): @@ -417,7 +417,7 @@ class PostProcessor(object): self.in_history = False # if we don't have either of these then there's nothing to use to search the history for anyway - if not self.nzb_name and not self.folder_name: + if not self.nzb_name and not self.file_name and not self.folder_name: return to_return # make a list of possible names to use in the search @@ -426,6 +426,10 @@ class PostProcessor(object): names.append(self.nzb_name) if '.' in self.nzb_name: names.append(self.nzb_name.rpartition('.')[0]) + if self.file_name: + names.append(self.file_name) + if '.' in self.file_name: + names.append(self.file_name.rpartition('.')[0]) if self.folder_name: names.append(self.folder_name) @@ -481,7 +485,7 @@ class PostProcessor(object): parse_result = np.parse(name) self._log(u'Parsed %s
    .. from %s' % (str(parse_result).decode('utf-8', 'xmlcharrefreplace'), name), logger.DEBUG) - if parse_result.is_air_by_date: + if parse_result.is_air_by_date and (None is parse_result.season_number or not parse_result.episode_numbers): season = -1 episodes = [parse_result.air_date] else: @@ -500,8 +504,9 @@ class PostProcessor(object): self.release_group = parse_result.release_group # remember whether it's a proper - if parse_result.extra_info: - self.is_proper = None is not re.search('(^|[\. _-])(proper|repack)([\. _-]|$)', parse_result.extra_info, re.I) + if parse_result.extra_info_no_name(): + self.is_proper = 0 < common.Quality.get_proper_level(parse_result.extra_info_no_name(), parse_result.version, + parse_result.is_anime) # if the result is complete then set release name if parse_result.series_name and\ @@ -652,7 +657,7 @@ class PostProcessor(object): """ # if there is a quality available in the status then we don't need to bother guessing from the filename - if ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER + common.Quality.SNATCHED_BEST: + if ep_obj.status in common.Quality.SNATCHED_ANY: old_status, ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) # @UnusedVariable if common.Quality.UNKNOWN != ep_quality: self._log( @@ -737,7 +742,7 @@ class PostProcessor(object): """ # if SickGear snatched this then assume it's safe - if ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER + common.Quality.SNATCHED_BEST: + if ep_obj.status in common.Quality.SNATCHED_ANY: self._log(u'SickGear snatched this episode, marking it safe to replace', logger.DEBUG) return True @@ -771,10 +776,27 @@ class PostProcessor(object): # if there's an existing downloaded file with same quality, check filesize to decide if new_ep_quality == old_ep_quality: - if (isinstance(self.nzb_name, basestring) and re.search(r'\bproper|repack\b', self.nzb_name, re.I)) or \ - (isinstance(self.file_name, basestring) and re.search(r'\bproper|repack\b', self.file_name, re.I)): - self._log(u'Proper or repack with same quality, marking it safe to replace', logger.DEBUG) - return True + np = NameParser(showObj=self.showObj) + cur_proper_level = 0 + try: + pr = np.parse(ep_obj.release_name) + cur_proper_level = common.Quality.get_proper_level(pr.extra_info_no_name(), pr.version, pr.is_anime) + except (StandardError, Exception): + pass + new_name = (('', self.file_name)[isinstance(self.file_name, basestring)], self.nzb_name)[isinstance( + self.nzb_name, basestring)] + if new_name: + try: + npr = np.parse(new_name) + except (StandardError, Exception): + npr = None + if npr: + is_repack, new_proper_level = common.Quality.get_proper_level(npr.extra_info_no_name(), npr.version, + npr.is_anime, check_is_repack=True) + if new_proper_level > cur_proper_level and \ + (not is_repack or npr.release_group == ep_obj.release_group): + self._log(u'Proper or repack with same quality, marking it safe to replace', logger.DEBUG) + return True self._log(u'An episode exists in the database with the same quality as the episode to process', logger.DEBUG) @@ -896,7 +918,7 @@ class PostProcessor(object): try: ek.ek(os.mkdir, ep_obj.show.location) # do the library update for synoindex - notifiers.synoindex_notifier.addFolder(ep_obj.show.location) + notifiers.NotifierFactory().get('SYNOINDEX').addFolder(ep_obj.show.location) except (OSError, IOError): raise exceptions.PostProcessingFailed(u'Unable to create show directory: ' + ep_obj.show.location) @@ -946,10 +968,10 @@ class PostProcessor(object): # Just want to keep this consistent for failed handling right now release_name = show_name_helpers.determineReleaseName(self.folder_path, self.nzb_name) - if None is not release_name: - failed_history.logSuccess(release_name) - else: + if None is release_name: self._log(u'No snatched release found in history', logger.WARNING) + elif sickbeard.USE_FAILED_DOWNLOADS: + failed_history.remove_failed(release_name) # find the destination folder try: @@ -1045,34 +1067,13 @@ class PostProcessor(object): ep_obj.createMetaFiles() # log it to history - history.logDownload(ep_obj, self.file_path, new_ep_quality, self.release_group, anime_version) + history.log_download(ep_obj, self.file_path, new_ep_quality, self.release_group, anime_version) # send notifications notifiers.notify_download(ep_obj._format_pattern('%SN - %Sx%0E - %EN - %QN')) - # do the library update for Emby - notifiers.emby_notifier.update_library(ep_obj.show) - - # do the library update for Kodi - notifiers.kodi_notifier.update_library(ep_obj.show.name) - - # do the library update for XBMC - notifiers.xbmc_notifier.update_library(ep_obj.show.name) - - # do the library update for Plex - notifiers.plex_notifier.update_library(ep_obj) - - # do the library update for NMJ - # nmj_notifier kicks off its library update when the notify_download is issued (inside notifiers) - - # do the library update for Synology Indexer - notifiers.synoindex_notifier.addFile(ep_obj.location) - - # do the library update for pyTivo - notifiers.pytivo_notifier.update_library(ep_obj) - - # do the library update for Trakt - notifiers.trakt_notifier.update_collection(ep_obj) + # trigger library updates + notifiers.notify_update_library(ep_obj=ep_obj) self._run_extra_scripts(ep_obj) diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py index 50f86c0e..b6b23f9d 100644 --- a/sickbeard/processTV.py +++ b/sickbeard/processTV.py @@ -35,7 +35,9 @@ from sickbeard.exceptions import ex from sickbeard import logger from sickbeard.name_parser.parser import NameParser, InvalidNameException, InvalidShowException from sickbeard import common +from sickbeard.common import SNATCHED_ANY from sickbeard.history import reset_status +from sickbeard.exceptions import MultipleShowObjectsException from sickbeard import failedProcessor @@ -56,12 +58,13 @@ except ImportError: class ProcessTVShow(object): """ Process a TV Show """ - def __init__(self, webhandler=None): + def __init__(self, webhandler=None, is_basedir=True): self.files_passed = 0 self.files_failed = 0 self.fail_detected = False self._output = [] self.webhandler = webhandler + self.is_basedir = is_basedir @property def any_vid_processed(self): @@ -161,7 +164,60 @@ class ProcessTVShow(object): return result - def process_dir(self, dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, failed=False, pp_type='auto', cleanup=False, showObj=None): + def check_name(self, name): + if self.is_basedir: + return None + + so = None + my_db = db.DBConnection() + sql_results = my_db.select( + 'SELECT showid FROM history' + + ' WHERE resource = ?' + + ' AND (%s)' % ' OR '.join('action LIKE "%%%02d"' % x for x in SNATCHED_ANY) + + ' ORDER BY rowid', [name]) + if sql_results: + try: + so = helpers.findCertainShow(sickbeard.showList, int(sql_results[-1]['showid'])) + if hasattr(so, 'name'): + logger.log('Found Show: %s in snatch history for: %s' % (so.name, name), logger.DEBUG) + except MultipleShowObjectsException: + so = None + return so + + def showObj_helper(self, showObj, base_dir, dir_name, nzb_name, pp_type, alt_showObj=None): + if None is showObj and base_dir == sickbeard.TV_DOWNLOAD_DIR and not nzb_name or 'manual' == pp_type: + # Scheduled Post Processing Active + return self.check_name(dir_name) + return (showObj, alt_showObj)[None is showObj and None is not alt_showObj] + + def check_video_filenames(self, path, videofiles): + if self.is_basedir: + return None + + video_pick = None + video_size = 0 + for cur_video_file in videofiles: + try: + cur_video_size = ek.ek(os.path.getsize, ek.ek(os.path.join, path, cur_video_file)) + except (StandardError, Exception): + continue + + if 0 == video_size or cur_video_size > video_size: + video_size = cur_video_size + video_pick = cur_video_file + + if video_pick: + vid_filename = ek.ek(os.path.splitext, video_pick)[0] + # check if filename is garbage, disregard it + if re.search(r'^[a-zA-Z0-9]+$', vid_filename): + return None + + return self.check_name(vid_filename) + + return None + + def process_dir(self, dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, + failed=False, pp_type='auto', cleanup=False, showObj=None): """ Scans through the files in dir_name and processes whatever media files it finds @@ -179,7 +235,8 @@ class ProcessTVShow(object): # if the client and SickGear are not on the same machine translate the directory in a network directory elif dir_name and sickbeard.TV_DOWNLOAD_DIR and ek.ek(os.path.isdir, sickbeard.TV_DOWNLOAD_DIR)\ and ek.ek(os.path.normpath, dir_name) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR): - dir_name = ek.ek(os.path.join, sickbeard.TV_DOWNLOAD_DIR, ek.ek(os.path.abspath, dir_name).split(os.path.sep)[-1]) + dir_name = ek.ek(os.path.join, sickbeard.TV_DOWNLOAD_DIR, + ek.ek(os.path.abspath, dir_name).split(os.path.sep)[-1]) self._log_helper(u'SickGear PP Config, completed TV downloads folder: ' + sickbeard.TV_DOWNLOAD_DIR) if dir_name: @@ -195,6 +252,16 @@ class ProcessTVShow(object): u'you fill out your completed TV download folder in the PP config.') return self.result + if dir_name == sickbeard.TV_DOWNLOAD_DIR: + self.is_basedir = True + + if None is showObj: + if isinstance(nzb_name, basestring): + showObj = self.check_name(re.sub(r'\.(nzb|torrent)$', '', nzb_name, flags=re.I)) + + if None is showObj and dir_name: + showObj = self.check_name(ek.ek(os.path.basename, dir_name)) + path, dirs, files = self._get_path_dir_files(dir_name, nzb_name, pp_type) if sickbeard.POSTPONE_IF_SYNC_FILES and any(filter(helpers.isSyncFile, files)): @@ -217,7 +284,9 @@ class ProcessTVShow(object): if self.fail_detected: self._process_failed(dir_name, nzb_name, showObj=showObj) return self.result + rar_content = [x for x in rar_content if not helpers.is_link(ek.ek(os.path.join, path, x))] path, dirs, files = self._get_path_dir_files(dir_name, nzb_name, pp_type) + files = [x for x in files if not helpers.is_link(ek.ek(os.path.join, path, x))] video_files = filter(helpers.has_media_ext, files) video_in_rar = filter(helpers.has_media_ext, rar_content) work_files += [ek.ek(os.path.join, path, item) for item in rar_content] @@ -236,11 +305,17 @@ class ProcessTVShow(object): if 2 <= len(video_files): nzb_name = None + if None is showObj and 0 < len(video_files): + showObj = self.check_video_filenames(path, video_files) + # self._set_process_success() # Don't Link media when the media is extracted from a rar in the same path if process_method in ('hardlink', 'symlink') and video_in_rar: - self._process_media(path, video_in_rar, nzb_name, 'move', force, force_replace, showObj=showObj) + soh = showObj + if None is showObj: + soh = self.check_video_filenames(path, video_in_rar) + self._process_media(path, video_in_rar, nzb_name, 'move', force, force_replace, showObj=soh) self._delete_files(path, [ek.ek(os.path.relpath, item, path) for item in work_files], force=True) video_batch = set(video_files) - set(video_in_rar) else: @@ -258,14 +333,17 @@ class ProcessTVShow(object): video_batch = set(video_batch) - set(video_pick) - self._process_media(path, video_pick, nzb_name, process_method, force, force_replace, use_trash=cleanup, showObj=showObj) + self._process_media(path, video_pick, nzb_name, process_method, force, force_replace, + use_trash=cleanup, showObj=showObj) except OSError as e: logger.log('Batch skipped, %s%s' % (ex(e), e.filename and (' (file %s)' % e.filename) or ''), logger.WARNING) # Process video files in TV subdirectories - for directory in [x for x in dirs if self._validate_dir(path, x, nzb_name_original, failed, showObj=showObj)]: + for directory in [x for x in dirs if self._validate_dir( + path, x, nzb_name_original, failed, + showObj=self.showObj_helper(showObj, dir_name, x, nzb_name, pp_type))]: # self._set_process_success(reset=True) @@ -275,13 +353,17 @@ class ProcessTVShow(object): self._log_helper(u'Found temporary sync files, skipping post process', logger.ERROR) return self.result + # Ignore any symlinks at this stage to avoid the potential for unraring a symlinked archive + files = [x for x in files if not helpers.is_link(ek.ek(os.path.join, walk_path, x))] + rar_files, rarfile_history = self.unused_archives( walk_path, filter(helpers.is_first_rar_volume, files), pp_type, process_method, rarfile_history) rar_content = self._unrar(walk_path, rar_files, force) work_files += [ek.ek(os.path.join, walk_path, item) for item in rar_content] if self.fail_detected: - self._process_failed(dir_name, nzb_name, showObj=showObj) + self._process_failed(dir_name, nzb_name, showObj=self.showObj_helper(showObj, directory)) continue + rar_content = [x for x in rar_content if not helpers.is_link(ek.ek(os.path.join, walk_path, x))] files = list(set(files + rar_content)) video_files = filter(helpers.has_media_ext, files) video_in_rar = filter(helpers.has_media_ext, rar_content) @@ -289,7 +371,9 @@ class ProcessTVShow(object): # Don't Link media when the media is extracted from a rar in the same path if process_method in ('hardlink', 'symlink') and video_in_rar: - self._process_media(walk_path, video_in_rar, nzb_name, 'move', force, force_replace, showObj=showObj) + self._process_media(walk_path, video_in_rar, nzb_name, 'move', force, force_replace, + showObj=self.showObj_helper(showObj, dir_name, directory, nzb_name, pp_type, + self.check_video_filenames(walk_dir, video_in_rar))) video_batch = set(video_files) - set(video_in_rar) else: video_batch = video_files @@ -307,7 +391,10 @@ class ProcessTVShow(object): video_batch = set(video_batch) - set(video_pick) - self._process_media(walk_path, video_pick, nzb_name, process_method, force, force_replace, use_trash=cleanup, showObj=showObj) + self._process_media( + walk_path, video_pick, nzb_name, process_method, force, force_replace, use_trash=cleanup, + showObj=self.showObj_helper(showObj, dir_name, directory, nzb_name, pp_type, + self.check_video_filenames(walk_dir, video_pick))) except OSError as e: logger.log('Batch skipped, %s%s' % @@ -557,7 +644,7 @@ class ProcessTVShow(object): 'media_pattern': re.compile('|'.join([ r'\.s\d{2}e\d{2}\.', r'\.(?:36|72|216)0p\.', r'\.(?:480|576|1080)[pi]\.', r'\.[xh]26[45]\b', r'\.bluray\.', r'\.[hp]dtv\.', r'\.web(?:[.-]?dl)?\.', r'\.(?:vhs|vod|dvd|web|bd|br).?rip\.', - r'\.dvdr\b', r'\.(?:stv|vcd)\.', r'\bhd(?:cam|rip)\b', r'\.(?:internal|proper|repack|screener)\.', + r'\.dvdr\b', r'\.(?:stv|vcd)\.', r'\bhd(?:cam|rip)\b', r'\.(?:internal|real|proper|repack|screener)\.', r'\b(?:aac|ac3|mp3)\b', r'\.(?:ntsc|pal|secam)\.', r'\.r5\.', r'\bscr\b', r'\b(?:divx|xvid)\b' ]), flags=re.IGNORECASE) } @@ -673,10 +760,10 @@ class ProcessTVShow(object): for wdata in iter(partial(part.read, 4096), b''): try: newfile.write(wdata) - except: + except (StandardError, Exception): logger.log('Failed write to file %s' % f) return result - except: + except (StandardError, Exception): logger.log('Failed read from file %s' % f) return result result = base_filepath @@ -700,22 +787,23 @@ class ProcessTVShow(object): pass if None is parse_result: try: - parse_result = NameParser(try_scene_exceptions=True,convert=True).parse(dir_name, cache_result=False) + parse_result = NameParser(try_scene_exceptions=True, convert=True).parse(dir_name, cache_result=False) except (InvalidNameException, InvalidShowException): # If the filename doesn't parse, then return false as last # resort. We can assume that unparseable filenames are not # processed in the past return False - showlink = ('for "%s"' % (sickbeard.WEB_ROOT, parse_result.show.indexerid, parse_result.show.name), - parse_result.show.name)[self.any_vid_processed] + showlink = ('for "%s"' % ( + sickbeard.WEB_ROOT, parse_result.show.indexerid, parse_result.show.name), + parse_result.show.name)[self.any_vid_processed] ep_detail_sql = '' if parse_result.show.indexerid and 0 < len(parse_result.episode_numbers) and parse_result.season_number: ep_detail_sql = " and tv_episodes.showid='%s' and tv_episodes.season='%s' and tv_episodes.episode='%s'"\ % (str(parse_result.show.indexerid), - str(parse_result.season_number), - str(parse_result.episode_numbers[0])) + str(parse_result.season_number), + str(parse_result.episode_numbers[0])) # Avoid processing the same directory again if we use a process method <> move my_db = db.DBConnection() @@ -734,7 +822,8 @@ class ProcessTVShow(object): if not isinstance(videofile, unicode): videofile = unicode(videofile, 'utf_8') - sql_result = my_db.select('SELECT * FROM tv_episodes WHERE release_name = ?', [videofile.rpartition('.')[0]]) + sql_result = my_db.select( + 'SELECT * FROM tv_episodes WHERE release_name = ?', [videofile.rpartition('.')[0]]) if sql_result: self._log_helper(u'Found a video, but that release %s was already processed,
    .. skipping: %s' % (showlink, videofile)) @@ -764,7 +853,8 @@ class ProcessTVShow(object): return False - def _process_media(self, process_path, video_files, nzb_name, process_method, force, force_replace, use_trash=False, showObj=None): + def _process_media(self, process_path, video_files, nzb_name, process_method, force, force_replace, + use_trash=False, showObj=None): processor = None for cur_video_file in video_files: @@ -776,7 +866,10 @@ class ProcessTVShow(object): cur_video_file_path = ek.ek(os.path.join, process_path, cur_video_file) try: - processor = postProcessor.PostProcessor(cur_video_file_path, nzb_name, process_method, force_replace, use_trash=use_trash, webhandler=self.webhandler, showObj=showObj) + processor = postProcessor.PostProcessor( + cur_video_file_path, nzb_name, process_method, force_replace, + use_trash=use_trash, webhandler=self.webhandler, showObj=showObj) + file_success = processor.process() process_fail_message = '' except exceptions.PostProcessingFailed: @@ -803,14 +896,17 @@ class ProcessTVShow(object): dirs = [] files = [] - if dir_name == sickbeard.TV_DOWNLOAD_DIR and not nzb_name or 'manual' == pp_type: # Scheduled Post Processing Active + if dir_name == sickbeard.TV_DOWNLOAD_DIR and not nzb_name or 'manual' == pp_type: + # Scheduled Post Processing Active # Get at first all the subdir in the dir_name for path, dirs, files in ek.ek(os.walk, dir_name): + files = [x for x in files if not helpers.is_link(ek.ek(os.path.join, path, x))] break else: path, dirs = ek.ek(os.path.split, dir_name) # Script Post Processing if None is not nzb_name and not nzb_name.endswith('.nzb') and \ - ek.ek(os.path.isfile, ek.ek(os.path.join, dir_name, nzb_name)): # For single torrent file without directory + ek.ek(os.path.isfile, ek.ek(os.path.join, dir_name, nzb_name)): + # For single torrent file without directory dirs = [] files = [ek.ek(os.path.join, dir_name, nzb_name)] else: @@ -850,6 +946,9 @@ class ProcessTVShow(object): # backward compatibility prevents the case of this function name from being updated to PEP8 -def processDir(dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, failed=False, type='auto', cleanup=False, webhandler=None, showObj=None): +def processDir(dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, + failed=False, type='auto', cleanup=False, webhandler=None, showObj=None, is_basedir=True): + # backward compatibility prevents the case of this function name from being updated to PEP8 - return ProcessTVShow(webhandler).process_dir(dir_name, nzb_name, process_method, force, force_replace, failed, type, cleanup, showObj) + return ProcessTVShow(webhandler, is_basedir).process_dir( + dir_name, nzb_name, process_method, force, force_replace, failed, type, cleanup, showObj) diff --git a/sickbeard/properFinder.py b/sickbeard/properFinder.py index 40a46d46..fb01453d 100644 --- a/sickbeard/properFinder.py +++ b/sickbeard/properFinder.py @@ -18,19 +18,19 @@ import datetime import operator +import os import threading import traceback +import re import sickbeard -from sickbeard import db -from sickbeard import exceptions -from sickbeard.exceptions import ex -from sickbeard import helpers, logger, show_name_helpers -from sickbeard import search -from sickbeard import history - -from sickbeard.common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, Quality, ARCHIVED, SNATCHED_BEST +from sickbeard import db, exceptions, helpers, history, logger, search, show_name_helpers +from sickbeard import encodingKludge as ek +from sickbeard.common import DOWNLOADED, SNATCHED_ANY, SNATCHED_PROPER, Quality, ARCHIVED, FAILED +from sickbeard.exceptions import ex, MultipleShowObjectsException +from sickbeard import failed_history +from sickbeard.history import dateFormat from name_parser.parser import NameParser, InvalidNameException, InvalidShowException @@ -72,13 +72,55 @@ def search_propers(): logger.log(u'Completed the search for new propers%s' % run_at) +def get_old_proper_level(showObj, indexer, indexerid, season, episodes, old_status, new_quality, + extra_no_name, version, is_anime=False): + level = 0 + is_internal = False + codec = '' + if old_status not in SNATCHED_ANY: + level = Quality.get_proper_level(extra_no_name, version, is_anime) + elif showObj: + myDB = db.DBConnection() + np = NameParser(False, showObj=showObj) + for episode in episodes: + result = myDB.select('SELECT resource FROM history WHERE showid = ? AND season = ? AND episode = ? AND ' + '(' + ' OR '.join("action LIKE '%%%02d'" % x for x in SNATCHED_ANY) + ') ' + 'ORDER BY date DESC LIMIT 1', + [indexerid, season, episode]) + if not result or not isinstance(result[0]['resource'], basestring) or not result[0]['resource']: + continue + nq = Quality.sceneQuality(result[0]['resource'], showObj.is_anime) + if nq != new_quality: + continue + try: + p = np.parse(result[0]['resource']) + except (StandardError, Exception): + continue + level = Quality.get_proper_level(p.extra_info_no_name(), p.version, showObj.is_anime) + is_internal = p.extra_info_no_name() and re.search(r'\binternal\b', p.extra_info_no_name(), flags=re.I) + codec = _get_codec(p.extra_info_no_name()) + break + return level, is_internal, codec + + +def _get_codec(extra_info_no_name): + if not extra_info_no_name: + return '' + if re.search(r'\b[xh]264\b', extra_info_no_name, flags=re.I): + return '264' + elif re.search(r'\bxvid\b', extra_info_no_name, flags=re.I): + return 'xvid' + elif re.search(r'\b[xh]265|hevc\b', extra_info_no_name, flags=re.I): + return 'hevc' + return '' + + def _get_proper_list(aired_since_shows, recent_shows, recent_anime): propers = {} # for each provider get a list of the orig_thread_name = threading.currentThread().name providers = [x for x in sickbeard.providers.sortedProviderList() if x.is_active()] - np = NameParser(False, try_scene_exceptions=True) for cur_provider in providers: if not recent_anime and cur_provider.anime_only: continue @@ -87,13 +129,14 @@ def _get_proper_list(aired_since_shows, recent_shows, recent_anime): logger.log(u'Searching for new PROPER releases') try: - found_propers = cur_provider.find_propers(search_date=aired_since_shows, shows=recent_shows, anime=recent_anime) + found_propers = cur_provider.find_propers(search_date=aired_since_shows, shows=recent_shows, + anime=recent_anime) except exceptions.AuthException as e: logger.log(u'Authentication error: ' + ex(e), logger.ERROR) continue except Exception as e: logger.log(u'Error while searching ' + cur_provider.name + ', skipping: ' + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) continue finally: threading.currentThread().name = orig_thread_name @@ -104,29 +147,43 @@ def _get_proper_list(aired_since_shows, recent_shows, recent_anime): name = _generic_name(x.name) if name not in propers: try: - np = NameParser(False, try_scene_exceptions=True, showObj=x.parsed_show) + np = NameParser(False, try_scene_exceptions=True, showObj=x.parsed_show, indexer_lookup=False) parse_result = np.parse(x.name) if parse_result.series_name and parse_result.episode_numbers and \ - parse_result.show.indexerid in recent_shows + recent_anime: + (parse_result.show.indexer, parse_result.show.indexerid) in recent_shows + recent_anime: + cur_size = getattr(x, 'size', None) + if failed_history.has_failed(x.name, cur_size, cur_provider.name): + continue logger.log(u'Found new proper: ' + x.name, logger.DEBUG) x.show = parse_result.show.indexerid x.provider = cur_provider + x.is_repack, x.properlevel = Quality.get_proper_level(parse_result.extra_info_no_name(), + parse_result.version, + parse_result.is_anime, + check_is_repack=True) + x.is_internal = parse_result.extra_info_no_name() and \ + re.search(r'\binternal\b', parse_result.extra_info_no_name(), flags=re.I) + x.codec = _get_codec(parse_result.extra_info_no_name()) propers[name] = x count += 1 except (InvalidNameException, InvalidShowException): continue - except Exception: + except (StandardError, Exception): continue cur_provider.log_result('Propers', count, '%s' % cur_provider.name) # take the list of unique propers and get it sorted by - sorted_propers = sorted(propers.values(), key=operator.attrgetter('date'), reverse=True) - verified_propers = [] + sorted_propers = sorted(propers.values(), key=operator.attrgetter('properlevel', 'date'), reverse=True) + verified_propers = set() for cur_proper in sorted_propers: - parse_result = np.parse(cur_proper.name) + np = NameParser(False, try_scene_exceptions=True, showObj=cur_proper.parsed_show, indexer_lookup=False) + try: + parse_result = np.parse(cur_proper.name) + except (StandardError, Exception): + continue # set the indexerid in the db to the show's indexerid cur_proper.indexerid = parse_result.show.indexerid @@ -139,7 +196,10 @@ def _get_proper_list(aired_since_shows, recent_shows, recent_anime): cur_proper.episode = parse_result.episode_numbers[0] cur_proper.release_group = parse_result.release_group cur_proper.version = parse_result.version + cur_proper.extra_info = parse_result.extra_info + cur_proper.extra_info_no_name = parse_result.extra_info_no_name cur_proper.quality = Quality.nameQuality(cur_proper.name, parse_result.is_anime) + cur_proper.is_anime = parse_result.is_anime # only get anime proper if it has release group and version if parse_result.is_anime: @@ -148,7 +208,7 @@ def _get_proper_list(aired_since_shows, recent_shows, recent_anime): logger.DEBUG) continue - if not show_name_helpers.pass_wordlist_checks(cur_proper.name, parse=False): + if not show_name_helpers.pass_wordlist_checks(cur_proper.name, parse=False, indexer_lookup=False): logger.log(u'Proper %s isn\'t a valid scene release that we want, ignoring it' % cur_proper.name, logger.DEBUG) continue @@ -166,45 +226,94 @@ def _get_proper_list(aired_since_shows, recent_shows, recent_anime): # check if we actually want this proper (if it's the right quality) my_db = db.DBConnection() - sql_results = my_db.select('SELECT status FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?', - [cur_proper.indexerid, cur_proper.season, cur_proper.episode]) + sql_results = my_db.select( + 'SELECT release_group, status, version, release_name FROM tv_episodes WHERE showid = ? AND indexer = ? ' + + 'AND season = ? AND episode = ?', + [cur_proper.indexerid, cur_proper.indexer, cur_proper.season, cur_proper.episode]) if not sql_results: continue # only keep the proper if we have already retrieved the same quality ep (don't get better/worse ones) + # don't take proper of the same level we already downloaded old_status, old_quality = Quality.splitCompositeStatus(int(sql_results[0]['status'])) - if old_status not in (DOWNLOADED, SNATCHED, SNATCHED_BEST, ARCHIVED) \ - or cur_proper.quality != old_quality: + cur_proper.is_repack, cur_proper.proper_level = Quality.get_proper_level(cur_proper.extra_info_no_name(), + cur_proper.version, + cur_proper.is_anime, + check_is_repack=True) + + old_release_group = sql_results[0]['release_group'] + # check if we want this release: same quality as current, current has correct status + # restrict other release group releases to proper's + if old_status not in SNATCHED_ANY + [DOWNLOADED, ARCHIVED] \ + or cur_proper.quality != old_quality \ + or (cur_proper.is_repack and cur_proper.release_group != old_release_group): + continue + + np = NameParser(False, try_scene_exceptions=True, showObj=parse_result.show, indexer_lookup=False) + try: + extra_info = np.parse(sql_results[0]['release_name']).extra_info_no_name() + except (StandardError, Exception): + extra_info = None + + old_proper_level, old_is_internal, old_codec = get_old_proper_level(parse_result.show, cur_proper.indexer, + cur_proper.indexerid, cur_proper.season, + parse_result.episode_numbers, old_status, + cur_proper.quality, extra_info, + cur_proper.version, cur_proper.is_anime) + if cur_proper.proper_level < old_proper_level: + continue + elif cur_proper.proper_level == old_proper_level: + if '264' == cur_proper.codec and 'xvid' == old_codec: + pass + elif old_is_internal and not cur_proper.is_internal: + pass + else: + continue + + log_same_grp = 'Skipping proper from release group: [%s], does not match existing release group: [%s] for [%s]'\ + % (cur_proper.release_group, old_release_group, cur_proper.name) + + # for webldls, prevent propers from different groups + if sickbeard.PROPERS_WEBDL_ONEGRP and \ + (old_quality in (Quality.HDWEBDL, Quality.FULLHDWEBDL, Quality.UHD4KWEB) or + (old_quality == Quality.SDTV and re.search(r'\Wweb.?(dl|rip|.[hx]26[45])\W', str(sql_results[0]['release_name']), re.I))) and \ + cur_proper.release_group != old_release_group: + logger.log(log_same_grp, logger.DEBUG) continue # check if we actually want this proper (if it's the right release group and a higher version) if parse_result.is_anime: - my_db = db.DBConnection() - sql_results = my_db.select( - 'SELECT release_group, version FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?', - [cur_proper.indexerid, cur_proper.season, cur_proper.episode]) old_version = int(sql_results[0]['version']) - old_release_group = (sql_results[0]['release_group']) - if -1 < old_version < cur_proper.version: logger.log(u'Found new anime v%s to replace existing v%s' % (cur_proper.version, old_version)) else: continue if cur_proper.release_group != old_release_group: - logger.log(u'Skipping proper from release group: %s, does not match existing release group: %s' % - (cur_proper.release_group, old_release_group)) + logger.log(log_same_grp, logger.DEBUG) continue # if the show is in our list and there hasn't been a proper already added for that particular episode # then add it to our list of propers - if cur_proper.indexerid != -1 and (cur_proper.indexerid, cur_proper.season, cur_proper.episode) not in map( - operator.attrgetter('indexerid', 'season', 'episode'), verified_propers): - logger.log(u'Found a proper that may be useful: %s' % cur_proper.name) - verified_propers.append(cur_proper) + if cur_proper.indexerid != -1: + if (cur_proper.indexerid, cur_proper.indexer, cur_proper.season, cur_proper.episode) not in map( + operator.attrgetter('indexerid', 'indexer', 'season', 'episode'), verified_propers): + logger.log(u'Found a proper that may be useful: %s' % cur_proper.name) + verified_propers.add(cur_proper) + else: + rp = set() + for vp in verified_propers: + if vp.indexer == cur_proper.indexer and vp.indexerid == cur_proper.indexerid and \ + vp.season == cur_proper.season and vp.episode == cur_proper.episode and \ + vp.proper_level < cur_proper.proper_level: + rp.add(vp) + if rp: + verified_propers = verified_propers - rp + logger.log(u'Found a proper that may be useful: %s' % cur_proper.name) + verified_propers.add(cur_proper) - return verified_propers + return list(verified_propers) def _download_propers(proper_list): @@ -218,7 +327,7 @@ def _download_propers(proper_list): history_results = my_db.select( 'SELECT resource FROM history ' + 'WHERE showid = ? AND season = ? AND episode = ? AND quality = ? AND date >= ? ' + - 'AND action IN (' + ','.join([str(x) for x in Quality.SNATCHED]) + ')', + 'AND (' + ' OR '.join("action LIKE '%%%02d'" % x for x in SNATCHED_ANY + [DOWNLOADED, ARCHIVED]) + ')', [cur_proper.indexerid, cur_proper.season, cur_proper.episode, cur_proper.quality, history_limit.strftime(history.dateFormat)]) @@ -241,7 +350,8 @@ def _download_propers(proper_list): is_same = False for result in history_results: # if the result exists in history already we need to skip it - if clean_proper_name == _generic_name(helpers.remove_non_release_groups(result['resource'])): + if clean_proper_name == _generic_name(helpers.remove_non_release_groups( + ek.ek(os.path.basename, result['resource']))): is_same = True break if is_same: @@ -257,6 +367,9 @@ def _download_propers(proper_list): result.name = cur_proper.name result.quality = cur_proper.quality result.version = cur_proper.version + result.properlevel = cur_proper.proper_level + result.is_repack = cur_proper.is_repack + result.puid = cur_proper.puid # snatch it search.snatch_episode(result, SNATCHED_PROPER) @@ -266,24 +379,28 @@ def _recent_history(aired_since_shows, aired_since_anime): recent_shows, recent_anime = [], [] - aired_since_shows = aired_since_shows.toordinal() - aired_since_anime = aired_since_anime.toordinal() - my_db = db.DBConnection() + sql_results = my_db.select( - 'SELECT s.show_name, e.showid, e.season, e.episode, e.status, e.airdate FROM tv_episodes AS e' + + 'SELECT DISTINCT s.indexer, s.indexer_id FROM history as h' + + ' INNER JOIN tv_episodes AS e ON (h.showid == e.showid AND h.season == e.season AND h.episode == e.episode)' + ' INNER JOIN tv_shows AS s ON (e.showid = s.indexer_id)' + - ' WHERE e.airdate >= %s' % min(aired_since_shows, aired_since_anime) + - ' AND (e.status IN (%s))' % ','.join([str(x) for x in Quality.DOWNLOADED + Quality.SNATCHED]) + ' WHERE h.date >= %s' % min(aired_since_shows, aired_since_anime).strftime(dateFormat) + + ' AND (%s)' % ' OR '.join(['h.action LIKE "%%%02d"' % x for x in SNATCHED_ANY + [DOWNLOADED, FAILED]]) ) for sqlshow in sql_results: - show = helpers.findCertainShow(sickbeard.showList, sqlshow['showid']) + try: + show = helpers.find_show_by_id(sickbeard.showList, {int(sqlshow['indexer']): int(sqlshow['indexer_id'])}) + except MultipleShowObjectsException: + continue if show: - if sqlshow['airdate'] >= aired_since_shows and not show.is_anime: - sqlshow['showid'] not in recent_shows and recent_shows.append(sqlshow['showid']) + if not show.is_anime: + (sqlshow['indexer'], sqlshow['indexer_id']) not in recent_shows and \ + recent_shows.append((sqlshow['indexer'], sqlshow['indexer_id'])) else: - sqlshow['showid'] not in recent_anime and show.is_anime and recent_anime.append(sqlshow['showid']) + (sqlshow['indexer'], sqlshow['indexer_id']) not in recent_anime and show.is_anime and \ + recent_anime.append((sqlshow['indexer'], sqlshow['indexer_id'])) return recent_shows, recent_anime @@ -313,7 +430,7 @@ def _get_last_proper_search(): try: last_proper_search = datetime.date.fromordinal(int(sql_results[0]['last_proper_search'])) - except: + except (StandardError, Exception): return datetime.date.fromordinal(1) return last_proper_search diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py index 6d1a4e86..979ed703 100755 --- a/sickbeard/providers/__init__.py +++ b/sickbeard/providers/__init__.py @@ -24,37 +24,35 @@ import sickbeard from . import generic from sickbeard import logger, encodingKludge as ek # usenet -from . import newznab, omgwtfnzbs, womble +from . import newznab, omgwtfnzbs # torrent -from . import alpharatio, beyondhd, bithdtv, bitmetv, btn, btscene, dh, extratorrent, \ - fano, filelist, freshontv, funfile, gftracker, grabtheinfo, hd4free, hdbits, hdspace, hdtorrents, \ - iptorrents, limetorrents, morethan, ncore, pisexy, pretome, privatehd, ptf, \ - rarbg, revtt, scc, scenetime, shazbat, speedcd, \ - thepiratebay, torlock, torrentday, torrenting, torrentleech, \ - torrentshack, torrentz2, transmithe_net, tvchaosuk, zooqle +from . import alpharatio, beyondhd, bithdtv, bitmetv, blutopia, btn, btscene, dh, \ + fano, filelist, funfile, gftracker, grabtheinfo, hd4free, hdbits, hdspace, hdtorrents, \ + iptorrents, limetorrents, magnetdl, morethan, nebulance, ncore, nyaa, pisexy, pretome, privatehd, ptf, \ + rarbg, revtt, scenehd, scenetime, shazbat, skytorrents, speedcd, \ + thepiratebay, torlock, torrentbytes, torrentday, torrenting, torrentleech, \ + torrentvault, torrentz2, tvchaosuk, wop, zooqle # anime -from . import anizb, nyaatorrents, tokyotoshokan +from . import anizb, tokyotoshokan # custom try: from . import custom01 -except: +except (StandardError, Exception): pass __all__ = ['omgwtfnzbs', - 'womble', 'alpharatio', 'anizb', 'beyondhd', 'bithdtv', 'bitmetv', + 'blutopia', 'btn', 'btscene', 'custom01', 'dh', - 'extratorrent', 'fano', 'filelist', - 'freshontv', 'funfile', 'gftracker', 'grabtheinfo', @@ -64,29 +62,33 @@ __all__ = ['omgwtfnzbs', 'hdtorrents', 'iptorrents', 'limetorrents', + 'magnetdl', 'morethan', + 'nebulance', 'ncore', + 'nyaa', 'pisexy', 'pretome', 'privatehd', 'ptf', 'rarbg', 'revtt', - 'scc', + 'scenehd', 'scenetime', 'shazbat', + 'skytorrents', 'speedcd', 'thepiratebay', 'torlock', + 'torrentbytes', 'torrentday', 'torrenting', 'torrentleech', - 'torrentshack', + 'torrentvault', 'torrentz2', - 'transmithe_net', 'tvchaosuk', + 'wop', 'zooqle', - 'nyaatorrents', 'tokyotoshokan', ] @@ -111,7 +113,13 @@ def sortedProviderList(): def makeProviderList(): - return [x.provider for x in [getProviderModule(y) for y in __all__] if x] + providers = [x.provider for x in [getProviderModule(y) for y in __all__] if x] + import browser_ua, zlib + headers = [1449593765] + for p in providers: + if abs(zlib.crc32(p.name)) + 40000400 in headers: + p.headers.update({'User-Agent': browser_ua.get_ua()}) + return providers def getNewznabProviderList(data): @@ -145,6 +153,7 @@ def getNewznabProviderList(data): providerDict[curDefault.name].search_fallback = curDefault.search_fallback providerDict[curDefault.name].enable_recentsearch = curDefault.enable_recentsearch providerDict[curDefault.name].enable_backlog = curDefault.enable_backlog + providerDict[curDefault.name].enable_scheduled_backlog = curDefault.enable_scheduled_backlog return filter(lambda x: x, providerList) @@ -157,10 +166,14 @@ def makeNewznabProvider(configString): search_fallback = 0 enable_recentsearch = 0 enable_backlog = 0 + enable_scheduled_backlog = 1 try: values = configString.split('|') - if len(values) == 9: + if len(values) == 10: + name, url, key, cat_ids, enabled, search_mode, search_fallback, enable_recentsearch, enable_backlog, \ + enable_scheduled_backlog = values + elif len(values) == 9: name, url, key, cat_ids, enabled, search_mode, search_fallback, enable_recentsearch, enable_backlog = values else: name = values[0] @@ -176,7 +189,7 @@ def makeNewznabProvider(configString): newProvider = newznab.NewznabProvider(name, url, key=key, cat_ids=cat_ids, search_mode=search_mode, search_fallback=search_fallback, enable_recentsearch=enable_recentsearch, - enable_backlog=enable_backlog) + enable_backlog=enable_backlog, enable_scheduled_backlog=enable_scheduled_backlog) newProvider.enabled = enabled == '1' return newProvider @@ -205,10 +218,14 @@ def makeTorrentRssProvider(configString): search_fallback = 0 enable_recentsearch = 0 enable_backlog = 0 + enable_scheduled_backlog = 1 try: values = configString.split('|') - if len(values) == 8: + if len(values) == 9: + name, url, cookies, enabled, search_mode, search_fallback, enable_recentsearch, enable_backlog, \ + enable_scheduled_backlog = values + elif len(values) == 8: name, url, cookies, enabled, search_mode, search_fallback, enable_recentsearch, enable_backlog = values else: name = values[0] @@ -225,7 +242,7 @@ def makeTorrentRssProvider(configString): return newProvider = torrentRss.TorrentRssProvider(name, url, cookies, search_mode, search_fallback, enable_recentsearch, - enable_backlog) + enable_backlog, enable_scheduled_backlog) newProvider.enabled = enabled == '1' return newProvider diff --git a/sickbeard/providers/alpharatio.py b/sickbeard/providers/alpharatio.py index 375a44bb..886dc01f 100644 --- a/sickbeard/providers/alpharatio.py +++ b/sickbeard/providers/alpharatio.py @@ -39,8 +39,7 @@ class AlphaRatioProvider(generic.TorrentProvider): 'search': self.url_base + 'torrents.php?searchstr=%s%s&' + '&'.join( ['tags_type=1', 'order_by=time', 'order_way=desc'] + ['filter_cat[%s]=1' % c for c in 1, 2, 3, 4, 5] + - ['action=basic', 'searchsubmit=1']), - 'get': self.url_base + '%s'} + ['action=basic', 'searchsubmit=1'])} self.url = self.urls['config_provider_home_uri'] diff --git a/sickbeard/providers/anizb.py b/sickbeard/providers/anizb.py index 3591f287..fc0d4fd7 100644 --- a/sickbeard/providers/anizb.py +++ b/sickbeard/providers/anizb.py @@ -65,7 +65,7 @@ class AnizbCache(tvcache.TVCache): tvcache.TVCache.__init__(self, this_provider) self.update_freq = 6 - def _cache_data(self): + def _cache_data(self, **kwargs): return self.provider.cache_data() diff --git a/sickbeard/providers/beyondhd.py b/sickbeard/providers/beyondhd.py index f4c42f31..c06d555e 100644 --- a/sickbeard/providers/beyondhd.py +++ b/sickbeard/providers/beyondhd.py @@ -39,7 +39,7 @@ class BeyondHDProvider(generic.TorrentProvider): self.url = self.urls['config_provider_home_uri'] - self.passkey, self.minseed, self.minleech = 3 * [None] + self.passkey, self.scene, self.minseed, self.minleech = 4 * [None] def _check_auth_from_data(self, data_json): @@ -92,9 +92,5 @@ class BeyondHDProvider(generic.TorrentProvider): return results - def _episode_strings(self, ep_obj, **kwargs): - - return generic.TorrentProvider._episode_strings(self, ep_obj, scene=False, **kwargs) - provider = BeyondHDProvider() diff --git a/sickbeard/providers/bithdtv.py b/sickbeard/providers/bithdtv.py index 478e3e51..7fa1345f 100644 --- a/sickbeard/providers/bithdtv.py +++ b/sickbeard/providers/bithdtv.py @@ -32,23 +32,28 @@ class BitHDTVProvider(generic.TorrentProvider): self.url_home = ['https://www.bit-hdtv.com/'] - self.url_vars = {'login_action': 'login.php', 'search': 'torrents.php?search=%s&%s', 'get': '%s'} - self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login_action': '%(home)s%(vars)s', - 'search': '%(home)s%(vars)s', 'get': '%(home)s%(vars)s'} + self.url_vars = {'login': 'getrss.php', 'search': 'torrents.php?search=%s&%s'} + self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login': '%(home)s%(vars)s', + 'search': '%(home)s%(vars)s'} self.categories = {'Season': [12], 'Episode': [4, 5, 10], 'anime': [1]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] - self.username, self.password, self.freeleech, self.minseed, self.minleech = 5 * [None] + self.digest, self.freeleech, self.minseed, self.minleech = 4 * [None] def _authorised(self, **kwargs): return super(BitHDTVProvider, self)._authorised( - logged_in=(lambda y=None: self.has_all_cookies(['h_sl', 'h_sp', 'h_su']))) and 'search' in self.urls + logged_in=(lambda y=None: all( + [(None is y or re.search('(?i)rss\slink', y)), + self.has_all_cookies(['su', 'sp', 'sl'], 'h_'), 'search' in self.urls] + + [(self.session.cookies.get('h_' + x) or 'sg!no!pw') in self.digest for x in 'su', 'sp', 'sl'])), + failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings')) @staticmethod def _has_signature(data=None): - return generic.TorrentProvider._has_signature(data) or (data and re.search(r'(?sim)([^<]*)
  • ', '\1', html) - with BS4Parser(html, 'html.parser', attr='width=750') as soup: - torrent_table = soup.find('table', attrs={'width': 750}) + html = '\s*([^<]*)\1 len(torrent_rows): @@ -113,5 +119,10 @@ class BitHDTVProvider(generic.TorrentProvider): return results + @staticmethod + def ui_string(key): + + return 'bithdtv_digest' == key and 'use... \'h_su=xx; h_sp=yy; h_sl=zz\'' or '' + provider = BitHDTVProvider() diff --git a/sickbeard/providers/bitmetv.py b/sickbeard/providers/bitmetv.py index 84d44792..20f7cac9 100644 --- a/sickbeard/providers/bitmetv.py +++ b/sickbeard/providers/bitmetv.py @@ -34,8 +34,7 @@ class BitmetvProvider(generic.TorrentProvider): self.urls = {'config_provider_home_uri': self.url_base, 'login': self.url_base + 'links.php', - 'search': self.url_base + 'browse.php?%s&search=%s', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php?%s&search=%s'} self.categories = {'shows': 0, 'anime': 86} # exclusively one cat per key diff --git a/sickbeard/providers/blutopia.py b/sickbeard/providers/blutopia.py new file mode 100644 index 00000000..bd60c9dc --- /dev/null +++ b/sickbeard/providers/blutopia.py @@ -0,0 +1,169 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +try: + from collections import OrderedDict +except ImportError: + from requests.compat import OrderedDict +import re +import traceback + +from . import generic +from sickbeard import logger +from sickbeard.bs4_parser import BS4Parser +from sickbeard.helpers import tryInt +from lib.unidecode import unidecode + + +class BlutopiaProvider(generic.TorrentProvider): + + def __init__(self): + generic.TorrentProvider.__init__(self, 'Blutopia') + + self.url_base = 'https://blutopia.xyz/' + self.urls = {'config_provider_home_uri': self.url_base, + 'login': self.url_base + 'torrents', + 'search': self.url_base + 'filter?%s' % '&'.join( + ['_token=%s', 'search=%s', 'categories[]=%s', 'freeleech=%s', 'doubleupload=%s', 'featured=%s', + 'username=', 'imdb=', 'tvdb=', 'tmdb=', 'sorting=created_at', 'qty=50', 'direction=desc'])} + + self.categories = {'Season': [2], 'Episode': [2], 'Cache': [2]} + + self.url = self.urls['config_provider_home_uri'] + + self.filter = [] + self.may_filter = OrderedDict([ + ('f0', ('not marked', False)), ('free', ('free', True)), + ('double', ('2x up', True)), ('feat', ('featured', True))]) + self.digest, self.token, self.resp, self.scene, self.minseed, self.minleech = 6 * [None] + + def logged_in(self, resp): + try: + self.token = re.findall('csrf\s*=\s*"([^"]+)', resp)[0] + self.resp = re.findall('(?sim)()', resp)[0] + except (IndexError, TypeError): + return False + return self.has_all_cookies('XSRF-TOKEN') + + def _authorised(self, **kwargs): + + return super(BlutopiaProvider, self)._authorised( + logged_in=lambda y=None: self.logged_in(y)) + + def _search_provider(self, search_params, **kwargs): + + results = [] + if not self._authorised(): + return results + + items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} + + rc = dict((k, re.compile('(?i)' + v)) + for (k, v) in {'info': 'torrents', 'get': '(.*?download)_check(.*)'}.items()) + log = '' + if self.filter: + non_marked = 'f0' in self.filter + # if search_any, use unselected to exclude, else use selected to keep + filters = ([f for f in self.may_filter if f in self.filter], + [f for f in self.may_filter if f not in self.filter])[non_marked] + filters += (((all([x in filters for x in 'free', 'double', 'feat']) and ['freedoublefeat'] or []) + + (all([x in filters for x in 'free', 'double']) and ['freedouble'] or []) + + (all([x in filters for x in 'feat', 'double']) and ['featdouble'] or [])), + ((not all([x not in filters for x in 'free', 'double', 'feat']) and ['freedoublefeat'] or []) + + (not all([x not in filters for x in 'free', 'double']) and ['freedouble'] or []) + + (not all([x not in filters for x in 'feat', 'double']) and ['featdouble'] or [])) + )[non_marked] + rc['filter'] = re.compile('(?i)^(%s)$' % '|'.join( + ['%s' % f for f in filters if (f in self.may_filter and self.may_filter[f][1]) or f])) + log = '%sing (%s) ' % (('keep', 'skipp')[non_marked], ', '.join( + [f in self.may_filter and self.may_filter[f][0] or f for f in filters])) + for mode in search_params.keys(): + if mode in ['Season', 'Episode']: + show_type = self.show.air_by_date and 'Air By Date' \ + or self.show.is_sports and 'Sports' or None + if show_type: + logger.log(u'Provider does not carry shows of type: [%s], skipping' % show_type, logger.DEBUG) + return results + + for search_string in search_params[mode]: + search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string + search_url = self.urls['search'] % ( + self.token, '+'.join(search_string.split()), self._categories_string(mode, ''), '', '', '') + + resp = self.get_url(search_url, json=True) + + cnt = len(items[mode]) + try: + if not resp or not resp.get('rows'): + raise generic.HaltParseException + + html = '%s' % \ + self.resp.replace('', '%s' % ''.join(resp.get('result', []))) + with BS4Parser(html, features=['html5lib', 'permissive']) as soup: + torrent_table = soup.find('table', class_='table') + torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') + + if 2 > len(torrent_rows): + raise generic.HaltParseException + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + if any(self.filter): + marked = ','.join([x.attrs.get('data-original-title', '').lower() for x in tr.find_all( + 'i', attrs={'class': ['text-gold', 'fa-diamond', 'fa-certificate']})]) + # noinspection PyTypeChecker + munged = ''.join(filter(marked.__contains__, ['free', 'double', 'feat'])) + if ((non_marked and rc['filter'].search(munged)) or + (not non_marked and not rc['filter'].search(munged))): + continue + try: + head = head if None is not head else self._header_row( + tr, {'seed': r'circle-up', 'leech': r'circle-down', 'size': r'fa-file'}) + seeders, leechers, size = [tryInt(n, n) for n in [ + cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] + if self._peers_fail(mode, seeders, leechers): + continue + + title = tr.find('a', href=rc['info'])['data-original-title'] + download_url = self._link(rc['get'].sub(r'\1\2', tr.find('a', href=rc['get'])['href'])) + except (AttributeError, TypeError, ValueError, IndexError): + continue + + if title and download_url: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(items[mode]) - cnt, log + search_url) + + results = self._sort_seeding(mode, results + items[mode]) + + return results + + @staticmethod + def ui_string(key): + + return 'blutopia_digest' == key and 'use... \'remember_web_xx=yy\'' or '' + + +provider = BlutopiaProvider() diff --git a/sickbeard/providers/btn.py b/sickbeard/providers/btn.py index 80aad729..288da4ae 100644 --- a/sickbeard/providers/btn.py +++ b/sickbeard/providers/btn.py @@ -21,7 +21,10 @@ import time from . import generic from sickbeard import helpers, logger, scene_exceptions, tvcache +from sickbeard.bs4_parser import BS4Parser +from sickbeard.exceptions import AuthException from sickbeard.helpers import tryInt +from lib.unidecode import unidecode try: import json @@ -35,26 +38,41 @@ class BTNProvider(generic.TorrentProvider): def __init__(self): generic.TorrentProvider.__init__(self, 'BTN') - self.url_base = 'https://broadcasthe.net' - self.url_api = 'https://api.btnapps.net' + self.url_base = 'https://broadcasthe.net/' + self.url_api = 'https://api.broadcasthe.net' + + self.urls = {'config_provider_home_uri': self.url_base, 'login': self.url_base + 'login.php', + 'search': self.url_base + 'torrents.php?searchstr=%s&action=basic&%s'} self.proper_search_terms = ['%.proper.%', '%.repack.%'] - self.url = self.url_base - self.api_key, self.minseed, self.minleech = 3 * [None] + self.categories = {'Season': [2], 'Episode': [1]} + self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] + + self.url = self.urls['config_provider_home_uri'] + + self.api_key, self.username, self.password, self.auth_html, self.minseed, self.minleech = 6 * [None] + self.ua = self.session.headers['User-Agent'] self.reject_m2ts = False - self.session.headers = {'Content-Type': 'application/json-rpc'} self.cache = BTNCache(self) def _authorised(self, **kwargs): return self._check_auth() + def _check_auth(self, **kwargs): + + if not self.api_key and not (self.username and self.password): + raise AuthException('Must set Api key or Username/Password for %s in config provider options' % self.name) + return True + def _search_provider(self, search_params, age=0, **kwargs): - self._check_auth() + self._authorised() + self.auth_html = None results = [] + api_up = True for mode in search_params.keys(): for search_param in search_params[mode]: @@ -66,6 +84,7 @@ class BTNProvider(generic.TorrentProvider): else: search_param and params.update(search_param) age and params.update(dict(age='<=%i' % age)) # age in seconds + search_string = 'tvdb' in params and '%s %s' % (params.pop('series'), params['name']) or '' json_rpc = (lambda param_dct, items_per_page=1000, offset=0: '{"jsonrpc": "2.0", "id": "%s", "method": "getTorrents", "params": ["%s", %s, %s, %s]}' % @@ -73,7 +92,14 @@ class BTNProvider(generic.TorrentProvider): self.api_key, json.dumps(param_dct), items_per_page, offset)) try: - response = helpers.getURL(self.url_api, post_data=json_rpc(params), session=self.session, json=True) + response = None + if api_up and self.api_key: + self.session.headers['Content-Type'] = 'application/json-rpc' + response = helpers.getURL( + self.url_api, post_data=json_rpc(params), session=self.session, json=True) + if not response: + api_up = False + results = self.html(mode, search_string, results) error_text = response['error']['message'] logger.log( ('Call Limit' in error_text @@ -81,9 +107,12 @@ class BTNProvider(generic.TorrentProvider): or u'Action prematurely ended. %(prov)s server error response = %(desc)s') % {'prov': self.name, 'desc': error_text}, logger.WARNING) return results + except AuthException: + logger.log('API looks to be down, add un/pw config detail to be used as a fallback', logger.WARNING) except (KeyError, Exception): - data_json = response and 'result' in response and response['result'] or {} + pass + data_json = response and 'result' in response and response['result'] or {} if data_json: found_torrents = 'torrents' in data_json and data_json['torrents'] or {} @@ -138,6 +167,81 @@ class BTNProvider(generic.TorrentProvider): ('search_param: ' + str(search_param), self.name)['Cache' == mode]) results = self._sort_seeding(mode, results) + break # search first tvdb item only + + return results + + def _authorised_html(self): + + if self.username and self.password: + return super(BTNProvider, self)._authorised( + post_params={'login': 'Log In!'}, logged_in=(lambda y='': 'casThe' in y[0:4096])) + raise AuthException('Password or Username for %s is empty in config provider options' % self.name) + + def html(self, mode, search_string, results): + + if 'Content-Type' in self.session.headers: + del (self.session.headers['Content-Type']) + setattr(self.session, 'reserved', {'headers': { + 'Accept': 'text/html, application/xhtml+xml, */*', 'Accept-Language': 'en-GB', + 'Cache-Control': 'no-cache', 'Referer': 'https://broadcasthe.net/login.php', 'User-Agent': self.ua}}) + self.headers = None + + if self.auth_html or self._authorised_html(): + del (self.session.reserved['headers']['Referer']) + if 'Referer' in self.session.headers: + del (self.session.headers['Referer']) + self.auth_html = True + + search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string + search_url = self.urls['search'] % (search_string, self._categories_string(mode, 'filter_cat[%s]=1')) + + html = helpers.getURL(search_url, session=self.session) + cnt = len(results) + try: + if not html or self._has_no_results(html): + raise generic.HaltParseException + + with BS4Parser(html, features=['html5lib', 'permissive']) as soup: + torrent_table = soup.find(id='torrent_table') + torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') + + if 2 > len(torrent_rows): + raise generic.HaltParseException + + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in { + 'cats': '(?i)cat\[(?:%s)\]' % self._categories_string(mode, template='', delimiter='|'), + 'get': 'download'}.items()) + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + try: + head = head if None is not head else self._header_row(tr) + seeders, leechers, size = [tryInt(n, n) for n in [ + cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] + if ((self.reject_m2ts and re.search(r'(?i)\[.*?m2?ts.*?\]', tr.get_text('', strip=True))) or + self._peers_fail(mode, seeders, leechers) or not tr.find('a', href=rc['cats'])): + continue + + title = tr.select('td span[title]')[0].attrs.get('title').strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) + except (AttributeError, TypeError, ValueError, KeyError, IndexError): + continue + + if title and download_url: + results.append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(results) - cnt, search_url) + + results = self._sort_seeding(mode, results) return results @@ -187,17 +291,21 @@ class BTNProvider(generic.TorrentProvider): if 1 == ep_obj.show.indexer: base_params['tvdb'] = ep_obj.show.indexerid + base_params['series'] = ep_obj.show.name search_params.append(base_params) # elif 2 == ep_obj.show.indexer: # current_params['tvrage'] = ep_obj.show.indexerid # search_params.append(current_params) - else: - name_exceptions = list( - set([helpers.sanitizeSceneName(a) for a in - scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]])) - for name in name_exceptions: - series_param = {'series': name} - series_param.update(base_params) + # else: + name_exceptions = list( + set([helpers.sanitizeSceneName(a) for a in + scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]])) + dedupe = [ep_obj.show.name.replace(' ', '.')] + for name in name_exceptions: + if name.replace(' ', '.') not in dedupe: + dedupe += [name.replace(' ', '.')] + series_param = base_params.copy() + series_param['series'] = name search_params.append(series_param) return [dict(Season=search_params)] @@ -228,18 +336,23 @@ class BTNProvider(generic.TorrentProvider): # search if 1 == ep_obj.show.indexer: base_params['tvdb'] = ep_obj.show.indexerid + base_params['series'] = ep_obj.show.name search_params.append(base_params) # elif 2 == ep_obj.show.indexer: # search_params['tvrage'] = ep_obj.show.indexerid # to_return.append(search_params) - else: + + # else: # add new query string for every exception - name_exceptions = list( - set([helpers.sanitizeSceneName(a) for a in - scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]])) - for name in name_exceptions: - series_param = {'series': name} - series_param.update(base_params) + name_exceptions = list( + set([helpers.sanitizeSceneName(a) for a in + scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]])) + dedupe = [ep_obj.show.name.replace(' ', '.')] + for name in name_exceptions: + if name.replace(' ', '.') not in dedupe: + dedupe += [name.replace(' ', '.')] + series_param = base_params.copy() + series_param['series'] = name search_params.append(series_param) return [dict(Episode=search_params)] @@ -271,7 +384,7 @@ class BTNCache(tvcache.TVCache): self.update_freq = 15 - def _cache_data(self): + def _cache_data(self, **kwargs): return self.provider.cache_data(age=self._getLastUpdate().timetuple(), min_time=self.update_freq) diff --git a/sickbeard/providers/btscene.py b/sickbeard/providers/btscene.py index 0b5b0709..405af906 100644 --- a/sickbeard/providers/btscene.py +++ b/sickbeard/providers/btscene.py @@ -126,7 +126,7 @@ class BTSceneProvider(generic.TorrentProvider): return results def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='.', **kwargs) + return super(BTSceneProvider, self)._episode_strings(ep_obj, sep_date='.', **kwargs) provider = BTSceneProvider() diff --git a/sickbeard/providers/dh.py b/sickbeard/providers/dh.py index 2cad9adb..51d15934 100644 --- a/sickbeard/providers/dh.py +++ b/sickbeard/providers/dh.py @@ -33,8 +33,7 @@ class DHProvider(generic.TorrentProvider): self.url_base = 'https://www.digitalhive.org/' self.urls = {'config_provider_home_uri': self.url_base, 'login': self.url_base + 'getrss.php', - 'search': self.url_base + 'browse.php?search=%s&%s&titleonly=1&incldead=%s', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php?search=%s&%s&titleonly=1&incldead=%s'} self.categories = {'Season': [34], 'Episode': [7, 32, 55, 57], 'anime': [2]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] diff --git a/sickbeard/providers/fano.py b/sickbeard/providers/fano.py index b8ea33a0..d56eb4d8 100644 --- a/sickbeard/providers/fano.py +++ b/sickbeard/providers/fano.py @@ -38,7 +38,7 @@ class FanoProvider(generic.TorrentProvider): self.url_base = 'https://www.fano.in/' self.urls = {'config_provider_home_uri': self.url_base, - 'login_action': self.url_base + 'login.php', 'get': self.url_base + '%s', + 'login_action': self.url_base + 'login.php', 'search': self.url_base + 'browse_old.php?search=%s&%s&incldead=0'} self.categories = {'Season': [49], 'Episode': [6, 23, 32, 35], 'anime': [27]} diff --git a/sickbeard/providers/filelist.py b/sickbeard/providers/filelist.py index 78b2328b..4e7e66ea 100644 --- a/sickbeard/providers/filelist.py +++ b/sickbeard/providers/filelist.py @@ -33,8 +33,7 @@ class FLProvider(generic.TorrentProvider): self.url_base = 'https://filelist.ro/' self.urls = {'config_provider_home_uri': self.url_base, 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'browse.php?search=%s&%s&incldead=0', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php?search=%s&%s&incldead=0'} self.categories = {'Season': [14], 'Episode': [13, 21, 23], 'anime': [24]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] diff --git a/sickbeard/providers/freshontv.py b/sickbeard/providers/freshontv.py deleted file mode 100644 index 7335dc09..00000000 --- a/sickbeard/providers/freshontv.py +++ /dev/null @@ -1,143 +0,0 @@ -# coding=utf-8 -# -# This file is part of SickGear. -# -# SickGear is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SickGear is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SickGear. If not, see . - -try: - from collections import OrderedDict -except ImportError: - from requests.compat import OrderedDict -import re -import traceback - -from . import generic -from sickbeard import logger -from sickbeard.bs4_parser import BS4Parser -from sickbeard.helpers import tryInt -from lib.unidecode import unidecode - - -class FreshOnTVProvider(generic.TorrentProvider): - - def __init__(self): - generic.TorrentProvider.__init__(self, 'FreshOnTV', cache_update_freq=20) - - self.url_base = 'https://freshon.tv/' - self.urls = {'config_provider_home_uri': self.url_base, - 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'browse.php?incldead=0&words=0&%s&search=%s', - 'get': self.url_base + '%s'} - - self.categories = {'shows': 0, 'anime': 235} - - self.url = self.urls['config_provider_home_uri'] - - self.filter = [] - self.may_filter = OrderedDict([ - ('f0', ('not marked', False, '')), ('f50', ('50%', True)), ('f100', ('100%', True))]) - self.username, self.password, self.minseed, self.minleech = 4 * [None] - - def _authorised(self, **kwargs): - - return super(FreshOnTVProvider, self)._authorised( - post_params={'form_tmpl': True}, - failed_msg=(lambda y=None: 'DDoS protection by CloudFlare' in y and - u'Unable to login to %s due to CloudFlare DDoS javascript check' or - 'Username does not exist' in y and - u'Invalid username or password for %s. Check settings' or - u'Failed to authenticate or parse a response from %s, abort provider')) - - def _search_provider(self, search_params, **kwargs): - - results = [] - if not self._authorised(): - return results - - items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} - - rc = dict((k, re.compile('(?i)' + v)) - for (k, v) in {'info': 'detail', 'get': 'download', 'name': '_name'}.items()) - log = '' - if self.filter: - non_marked = 'f0' in self.filter - # if search_any, use unselected to exclude, else use selected to keep - filters = ([f for f in self.may_filter if f in self.filter], - [f for f in self.may_filter if f not in self.filter])[non_marked] - rc['filter'] = re.compile('(?i)(%s).png' % '|'.join( - [f.replace('f', '') for f in filters if self.may_filter[f][1]])) - log = '%sing (%s) ' % (('keep', 'skipp')[non_marked], ', '.join([self.may_filter[f][0] for f in filters])) - for mode in search_params.keys(): - for search_string in search_params[mode]: - - search_string, void = self._title_and_url(( - isinstance(search_string, unicode) and unidecode(search_string) or search_string, '')) - void, search_url = self._title_and_url(( - '', self.urls['search'] % (self._categories_string(mode, 'cat=%s'), search_string))) - - # returns top 15 results by default, expandable in user profile to 100 - html = self.get_url(search_url) - - cnt = len(items[mode]) - try: - if not html or self._has_no_results(html): - raise generic.HaltParseException - - with BS4Parser(html, features=['html5lib', 'permissive']) as soup: - torrent_table = soup.find('table', class_='frame') - torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') - - if 2 > len(torrent_rows): - raise generic.HaltParseException - - head = None - for tr in torrent_rows[1:]: - cells = tr.find_all('td') - if (5 > len(cells) or tr.find('img', alt='Nuked') - or (any(self.filter) - and ((non_marked and tr.find('img', src=rc['filter'])) - or (not non_marked and not tr.find('img', src=rc['filter']))))): - continue - try: - head = head if None is not head else self._header_row(tr) - seeders, leechers, size = [tryInt(n, n) for n in [ - cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] - if self._peers_fail(mode, seeders, leechers): - continue - - info = tr.find('a', href=rc['info'], class_=rc['name']) - title = (info.attrs.get('title') or info.get_text()).strip() - download_url = self._link(tr.find('a', href=rc['get'])['href']) - except (AttributeError, TypeError, ValueError): - continue - - if title and download_url: - items[mode].append((title, download_url, seeders, self._bytesizer(size))) - - except generic.HaltParseException: - pass - except (StandardError, Exception): - logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) - self._log_search(mode, len(items[mode]) - cnt, log + search_url) - - results = self._sort_seeding(mode, results + items[mode]) - - return results - - def _episode_strings(self, ep_obj, **kwargs): - - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='|', **kwargs) - - -provider = FreshOnTVProvider() diff --git a/sickbeard/providers/funfile.py b/sickbeard/providers/funfile.py index b82d5e64..d57bc5cd 100644 --- a/sickbeard/providers/funfile.py +++ b/sickbeard/providers/funfile.py @@ -34,8 +34,7 @@ class FunFileProvider(generic.TorrentProvider): self.url_base = 'https://www.funfile.org/' self.urls = {'config_provider_home_uri': self.url_base, 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'browse.php?%s&search=%s&incldead=0&showspam=1', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php?%s&search=%s&incldead=0&showspam=1'} self.categories = {'shows': [7], 'anime': [44]} diff --git a/sickbeard/providers/generic.py b/sickbeard/providers/generic.py index 8e1bb84a..7339109a 100644 --- a/sickbeard/providers/generic.py +++ b/sickbeard/providers/generic.py @@ -26,6 +26,7 @@ import os import re import time import urlparse +import threading from urllib import quote_plus import zlib from base64 import b16encode, b32decode @@ -33,6 +34,7 @@ from base64 import b16encode, b32decode import sickbeard import requests import requests.cookies +from cfscrape import CloudflareScraper from hachoir_parser import guessParser from hachoir_core.error import HachoirError from hachoir_core.stream import FileInputStream @@ -70,11 +72,12 @@ class GenericProvider: self.enabled = False self.enable_recentsearch = False self.enable_backlog = False + self.enable_scheduled_backlog = True self.categories = None self.cache = tvcache.TVCache(self) - self.session = requests.session() + self.session = CloudflareScraper.create_scraper() self.headers = { # Using USER_AGENT instead of Mozilla to keep same user agent along authentication and download phases, @@ -107,7 +110,7 @@ class GenericProvider: def is_public_access(self): try: - return bool(re.search('(?i)rarbg|sick|womble|anizb', self.name)) \ + return bool(re.search('(?i)rarbg|sick|anizb', self.name)) \ or False is bool(('_authorised' in self.__class__.__dict__ or hasattr(self, 'digest') or self._check_auth(is_required=True))) except AuthException: @@ -175,19 +178,22 @@ class GenericProvider: final_dir = sickbeard.TORRENT_DIR link_type = 'magnet' try: - torrent_hash = re.findall('(?i)urn:btih:([0-9a-f]{32,40})', result.url)[0].upper() + btih = None + try: + btih = re.findall('urn:btih:([\w]{32,40})', result.url)[0] + if 32 == len(btih): + from base64 import b16encode, b32decode + btih = b16encode(b32decode(btih)) + except (StandardError, Exception): + pass - if 32 == len(torrent_hash): - torrent_hash = b16encode(b32decode(torrent_hash)).lower() - - if not torrent_hash: + if not btih or not re.search('(?i)[0-9a-f]{32,40}', btih): logger.log('Unable to extract torrent hash from link: ' + ex(result.url), logger.ERROR) return False - urls = ['http%s://%s/torrent/%s.torrent' % (u + (torrent_hash,)) - for u in (('s', 'itorrents.org'), ('s', 'torra.pro'), ('s', 'torra.click'), - ('s', 'torrage.info'), ('', 'reflektor.karmorra.info'), - ('s', 'torrentproject.se'), ('', 'thetorrent.org'))] + urls = ['http%s://%s/torrent/%s.torrent' % (u + (btih.upper(),)) + for u in (('s', 'itorrents.org'), ('s', 'torrage.info'), ('', 'reflektor.karmorra.info'), + ('s', 'torrentproject.se'), ('', 'thetorrent.org'), ('s', 'torcache.to'))] except (StandardError, Exception): link_type = 'torrent' urls = [result.url] @@ -205,20 +211,24 @@ class GenericProvider: for url in urls: cache_dir = sickbeard.CACHE_DIR or helpers._getTempDir() base_name = '%s.%s' % (helpers.sanitizeFileName(result.name), self.providerType) + final_file = ek.ek(os.path.join, final_dir, base_name) + cached = getattr(result, 'cache_file', None) + if cached and ek.ek(os.path.isfile, cached): + base_name = ek.ek(os.path.basename, cached) cache_file = ek.ek(os.path.join, cache_dir, base_name) self.session.headers['Referer'] = url - if helpers.download_file(url, cache_file, session=self.session): + if cached or helpers.download_file(url, cache_file, session=self.session): if self._verify_download(cache_file): logger.log(u'Downloaded %s result from %s' % (self.name, url)) - final_file = ek.ek(os.path.join, final_dir, base_name) try: helpers.moveFile(cache_file, final_file) msg = 'moved' except (OSError, Exception): msg = 'copied cached file' - logger.log(u'Saved %s link and %s to %s' % (link_type, msg, final_file)) + logger.log(u'Saved .%s data and %s to %s' % ( + (link_type, 'torrent cache')['magnet' == link_type], msg, final_file)) saved = True break @@ -231,9 +241,7 @@ class GenericProvider: del(self.session.headers['Referer']) if not saved and 'magnet' == link_type: - logger.log(u'All torrent cache servers failed to return a downloadable result', logger.ERROR) - logger.log(u'Advice: in search settings, change from method blackhole to direct torrent client connect', - logger.ERROR) + logger.log(u'All torrent cache servers failed to return a downloadable result', logger.DEBUG) final_file = ek.ek(os.path.join, final_dir, '%s.%s' % (helpers.sanitizeFileName(result.name), link_type)) try: with open(final_file, 'wb') as fp: @@ -241,9 +249,11 @@ class GenericProvider: fp.flush() os.fsync(fp.fileno()) logger.log(u'Saved magnet link to file as some clients (or plugins) support this, %s' % final_file) - + if 'blackhole' == sickbeard.TORRENT_METHOD: + logger.log('Tip: If your client fails to load magnet in files, ' + + 'change blackhole to a client connection method in search settings') except (StandardError, Exception): - pass + logger.log(u'Failed to save magnet link to file, %s' % final_file) elif not saved: logger.log(u'Server failed to return anything useful', logger.ERROR) @@ -319,16 +329,16 @@ class GenericProvider: def _link(self, url, url_tmpl=None): url = url and str(url).strip().replace('&', '&') or '' - try: - url_tmpl = url_tmpl or self.urls['get'] - except (StandardError, Exception): - url_tmpl = '%s' - return url if re.match('(?i)https?://', url) else (url_tmpl % url.lstrip('/')) + return url if re.match('(?i)(https?://|magnet:)', url) \ + else (url_tmpl or self.urls.get('get', (getattr(self, 'url', '') or + getattr(self, 'url_base')) + '%s')) % url.lstrip('/') - def _header_row(self, table_row, custom_match=None, header_strip=''): + @staticmethod + def _header_row(table_row, custom_match=None, custom_tags=None, header_strip=''): """ :param header_row: Soup resultset of table header row :param custom_match: Dict key/values to override one or more default regexes + :param custom_tags: List of tuples with tag and attribute :param header_strip: String regex of ambiguities to remove from headers :return: dict column indices or None for leech, seeds, and size """ @@ -353,7 +363,7 @@ class GenericProvider: cell.find(tag, **p) for p in [{attr: rc[x]} for x in rc.keys()]]))), {}).get(attr) for (tag, attr) in [ ('img', 'title'), ('img', 'src'), ('i', 'title'), ('i', 'class'), - ('abbr', 'title'), ('a', 'title'), ('a', 'href')]]))), '') + ('abbr', 'title'), ('a', 'title'), ('a', 'href')] + (custom_tags or [])]))), '') or cell.get_text() )).strip() for cell in all_cells] headers = [re.sub(header_strip, '', x) for x in headers] @@ -408,6 +418,9 @@ class GenericProvider: def get_show(self, item, **kwargs): return None + def get_size_uid(self, item, **kwargs): + return -1, None + def find_search_results(self, show, episodes, search_mode, manual_search=False, **kwargs): self._check_auth() @@ -475,7 +488,7 @@ class GenericProvider: for item in item_list: (title, url) = self._title_and_url(item) - parser = NameParser(False, showObj=self.get_show(item, **kwargs), convert=True) + parser = NameParser(False, showObj=self.get_show(item, **kwargs), convert=True, indexer_lookup=False) # parse the file name try: parse_result = parser.parse(title) @@ -513,9 +526,8 @@ class GenericProvider: logger.log(u'The result ' + title + u' doesn\'t seem to be a valid season that we are trying' + u' to snatch, ignoring', logger.DEBUG) add_cache_entry = True - elif len(parse_result.episode_numbers)\ - and not [ep for ep in episodes - if ep.season == parse_result.season_number and + elif len(parse_result.episode_numbers) and not [ + ep for ep in episodes if ep.season == parse_result.season_number and ep.episode in parse_result.episode_numbers]: logger.log(u'The result ' + title + ' doesn\'t seem to be a valid episode that we are trying' + u' to snatch, ignoring', logger.DEBUG) @@ -531,20 +543,15 @@ class GenericProvider: u' didn\'t parse as one, skipping it', logger.DEBUG) add_cache_entry = True else: - airdate = parse_result.air_date.toordinal() - my_db = db.DBConnection() - sql_results = my_db.select('SELECT season, episode FROM tv_episodes ' + - 'WHERE showid = ? AND airdate = ?', [show_obj.indexerid, airdate]) + actual_season = parse_result.season_number + actual_episodes = parse_result.episode_numbers - if 1 != len(sql_results): - logger.log(u'Tried to look up the date for the episode ' + title + ' but the database didn\'t' + - u' give proper results, skipping it', logger.WARNING) + if not actual_episodes or \ + not [ep for ep in episodes if ep.season == actual_season and ep.episode in actual_episodes]: + logger.log(u'The result ' + title + ' doesn\'t seem to be a valid episode that we are trying' + + u' to snatch, ignoring', logger.DEBUG) add_cache_entry = True - if not add_cache_entry: - actual_season = int(sql_results[0]['season']) - actual_episodes = [int(sql_results[0]['episode'])] - # add parsed result to cache for usage later on if add_cache_entry: logger.log(u'Adding item from search to cache: ' + title, logger.DEBUG) @@ -581,6 +588,10 @@ class GenericProvider: result.release_group = release_group result.content = None result.version = version + result.size, result.puid = self.get_size_uid(item, **kwargs) + result.is_repack, result.properlevel = Quality.get_proper_level(parse_result.extra_info_no_name(), + parse_result.version, show_obj.is_anime, + check_is_repack=True) if 1 == len(ep_obj): ep_num = ep_obj[0].episode @@ -645,7 +656,7 @@ class GenericProvider: if hasattr(self, 'cookies'): cookies = self.cookies - if not (cookies and re.match('^(\w+=\w+[;\s]*)+$', cookies)): + if not (cookies and re.match('^(?:\w+=[^;\s]+[;\s]*)+$', cookies)): return False cj = requests.utils.add_dict_to_cookiejar(self.session.cookies, @@ -694,6 +705,20 @@ class GenericProvider: pass return long(math.ceil(value)) + @staticmethod + def _should_stop(): + if getattr(threading.currentThread(), 'stop', False): + return True + return False + + def _sleep_with_stop(self, t): + t_l = t + while t_l > 0: + time.sleep(3) + t_l -= 3 + if self._should_stop(): + return + class NZBProvider(object, GenericProvider): @@ -718,7 +743,7 @@ class NZBProvider(object, GenericProvider): if has_key: return has_key if None is has_key: - raise AuthException('%s for %s is empty in config provider options' + raise AuthException('%s for %s is empty in Media Providers/Options' % ('API key' + ('', ' and/or Username')[hasattr(self, 'username')], self.name)) return GenericProvider._check_auth(self) @@ -736,8 +761,8 @@ class NZBProvider(object, GenericProvider): search_terms = [] regex = [] if shows: - search_terms += ['.proper.', '.repack.'] - regex += ['proper|repack'] + search_terms += ['.proper.', '.repack.', '.real.'] + regex += ['proper|repack', Quality.real_check] proper_check = re.compile(r'(?i)(\b%s\b)' % '|'.join(regex)) if anime: terms = 'v1|v2|v3|v4|v5' @@ -791,7 +816,7 @@ class NZBProvider(object, GenericProvider): class TorrentProvider(object, GenericProvider): - def __init__(self, name, supports_backlog=True, anime_only=False, cache_update_freq=None): + def __init__(self, name, supports_backlog=True, anime_only=False, cache_update_freq=None, update_freq=None): GenericProvider.__init__(self, name, supports_backlog, anime_only) self.providerType = GenericProvider.TORRENT @@ -803,6 +828,8 @@ class TorrentProvider(object, GenericProvider): self.cache._cache_data = self._cache_data if cache_update_freq: self.cache.update_freq = cache_update_freq + self.ping_freq = update_freq + self.ping_skip = None @property def url(self): @@ -926,7 +953,7 @@ class TorrentProvider(object, GenericProvider): search_params = [] crop = re.compile(r'([.\s])(?:\1)+') for name in set(allPossibleShowNames(self.show)): - if process_name: + if process_name and getattr(self, 'scene', True): name = helpers.sanitizeSceneName(name) for detail in ep_detail: search_params += [crop.sub(r'\1', '%s %s%s' % (name, x, detail)) for x in prefix] @@ -934,8 +961,8 @@ class TorrentProvider(object, GenericProvider): @staticmethod def _has_signature(data=None): - return data and re.search(r'(?sim). +import base64 import re import traceback @@ -31,11 +32,25 @@ class IPTorrentsProvider(generic.TorrentProvider): generic.TorrentProvider.__init__(self, 'IPTorrents') self.url_home = (['https://iptorrents.%s/' % u for u in 'eu', 'com', 'me', 'ru'] + - ['http://11111.workisboring.com/', 'https://ipt-update.com']) + ['http://rss.workisboring.com/', 'https://ipt-update.com'] + + [base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('(?i)[q\s1]+', '', x[::-1]) for x in [ + 'c0RHa', 'vo1QD', 'hJ2L', 'GdhdXe', 'vdnLoN', 'J21cptmc', '5yZulmcv', '02bj', '=iq=']], + [re.sub('(?i)[q\seg]+', '', x[::-1]) for x in [ + 'RqHEa', 'LvEoDc0', 'Zvex2', 'LuF2', 'NXdu Vn', 'XZwQxeWY1', 'Yu42bzJ', 'tgG92']], + [re.sub('(?i)[q\sek]+', '', x[::-1]) for x in [ + 'H qa', 'vQoDc0R', '2L ', 'bod', 'hNmLk0N3', 'WLlxemY', 'LtVGZv1', 'wZy9m', '=kQ=']], + [re.sub('(?i)[q\seg1]+', '', x[::-1]) for x in [ + 'HGa', 'voDc0R', '21L', 'bucmbvt', 'ZyZWQ1L0Vm', 'ycrFW', '02bej5', 'e=gq']], + [re.sub('(?i)[q\sei]+', '', x[::-1]) for x in [ + 'Q0RHa', 'voiQDc', 'asF2L', 'hVmLuVW', 'yZulGd', 'mbhdmcv1', 'Adl5mLjl', '==Qe']], + [re.sub('(?i)[q\si1g]+', '', x[::-1]) for x in [ + 'Dc0GRHa', 'vo', 'Cdwl2L', 'FWZy5', 'bvJWL1k', '9mLzt2', 'wZy', '=GG=q']] + ]]]) - self.url_vars = {'login': 't', 'search': 't?%s;q=%s;qf=ti%s%s#torrents', 'get': '%s'} + self.url_vars = {'login': 't', 'search': 't?%s;q=%s;qf=ti%s%s#torrents'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login': '%(home)s%(vars)s', - 'search': '%(home)s%(vars)s', 'get': '%(home)s%(vars)s'} + 'search': '%(home)s%(vars)s'} self.categories = {'shows': [4, 5, 22, 23, 24, 25, 26, 55, 65, 66, 78, 79, 99], 'anime': [60]} @@ -47,7 +62,7 @@ class IPTorrentsProvider(generic.TorrentProvider): return super(IPTorrentsProvider, self)._authorised( logged_in=(lambda y='': all( - ['IPTorrents' in y, self.has_all_cookies()] + + ['IPTorrents' in y, 'type="password"' not in y[0:2048], self.has_all_cookies()] + [(self.session.cookies.get(x) or 'sg!no!pw') in self.digest for x in 'uid', 'pass'])), failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings')) diff --git a/sickbeard/providers/limetorrents.py b/sickbeard/providers/limetorrents.py index 49e0db2f..7d55dad7 100644 --- a/sickbeard/providers/limetorrents.py +++ b/sickbeard/providers/limetorrents.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import base64 import re import traceback import urllib @@ -31,7 +32,21 @@ class LimeTorrentsProvider(generic.TorrentProvider): def __init__(self): generic.TorrentProvider.__init__(self, 'LimeTorrents') - self.url_home = ['https://www.limetorrents.cc/', 'https://limetorrents.usbypass.xyz/'] + self.url_home = ['https://www.limetorrents.cc/'] + \ + ['https://%s/' % base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('[f\sX]+', '', x[::-1]) for x in [ + 'tlXGfb', '1X5SfZ', 'sfJfmb', 'rN 2Xb', 'u QfWZ', 's9G b']], + [re.sub('[ \ss]+', '', x[::-1]) for x in [ + 'Ztl Gsb', 'nc svRX', 'Rs nblJ', '5 JmLz', 'czsFsGc', 'nLskVs2', '0s N']], + [re.sub('[1\sF]+', '', x[::-1]) for x in [ + 'X Zt1lGb', 'l1Jn1cvR', 'mL11zRnb', 'uVXbtFFl', 'Hdp NWFa', '=1FQ3cuk']], + [re.sub('[y\sW]+', '', x[::-1]) for x in [ + 'XWZtlyGb', 'lJnyWcvR', 'nyLzRn b', 'vxmYWuWV', 'CWZlt2yY', '== Adyz5']], + [re.sub('[j\sy]+', '', x[::-1]) for x in [ + 'XyZtlG b', 'lJjnjcvR', 'njLz Rnb', 'vjxmYyuV', 'Gbyhjt2Y', 'n jJ3buw']], + [re.sub('[o\sg]+', '', x[::-1]) for x in [ + 'XZt lgGb', 'loJn cvR', 'ngLz Rnb', 'v xgmYuV', 'Gbh t2gY', '6l Heu w']], + ]]] self.url_vars = {'search': 'search/tv/%s/', 'browse': 'browse-torrents/TV-shows/'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'search': '%(home)s%(vars)s', diff --git a/sickbeard/providers/magnetdl.py b/sickbeard/providers/magnetdl.py new file mode 100644 index 00000000..cdd762a4 --- /dev/null +++ b/sickbeard/providers/magnetdl.py @@ -0,0 +1,103 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import re +import traceback + +from . import generic +from sickbeard import logger +from sickbeard.bs4_parser import BS4Parser +from sickbeard.helpers import tryInt +from lib.unidecode import unidecode + + +class MagnetDLProvider(generic.TorrentProvider): + + def __init__(self): + + generic.TorrentProvider.__init__(self, 'MagnetDL', cache_update_freq=6) + + self.url_base = 'http://www.magnetdl.com/' + + self.urls = {'config_provider_home_uri': self.url_base, + 'browse': self.url_base + 'download/tv/', 'search': self.url_base + 'search/?m=1&q=%s'} + + self.minseed, self.minleech = 2 * [None] + + def _search_provider(self, search_params, **kwargs): + + results = [] + + items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} + + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'info': '^/file/', 'get': '^magnet:'}.items()) + + for mode in search_params.keys(): + for search_string in search_params[mode]: + search_url = self.urls['browse'] + if 'Cache' != mode: + search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string + search_url = self.urls['search'] % re.sub('[.\s]+', ' ', search_string) + + html = self.get_url(search_url) + + cnt = len(items[mode]) + try: + if not html or self._has_no_results(html): + raise generic.HaltParseException + + with BS4Parser(html, features=['html5lib', 'permissive']) as soup: + torrent_table = soup.find('table', attrs={'class': 'download'}) + torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') + + if 2 > len(torrent_rows): + raise generic.HaltParseException + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + try: + head = head if None is not head else self._header_row(tr) + seeders, leechers, size = [tryInt(n, n) for n in [ + cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] + if self._peers_fail(mode, seeders, leechers): + continue + + info = tr.find('a', href=rc['info']) + title = (info.attrs.get('title') or info.get_text()).strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) + except (AttributeError, TypeError, ValueError, KeyError): + continue + + if title and download_url: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(items[mode]) - cnt, search_url) + + results = self._sort_seeding(mode, results + items[mode]) + + return results + + +provider = MagnetDLProvider() diff --git a/sickbeard/providers/morethan.py b/sickbeard/providers/morethan.py index eeabc768..2e3833b6 100644 --- a/sickbeard/providers/morethan.py +++ b/sickbeard/providers/morethan.py @@ -37,8 +37,7 @@ class MoreThanProvider(generic.TorrentProvider): 'login_action': self.url_base + 'login.php', 'search': self.url_base + 'torrents.php?searchstr=%s&' + '&'.join([ 'tags_type=1', 'order_by=time', 'order_way=desc', - 'filter_cat[2]=1', 'action=basic', 'searchsubmit=1']), - 'get': self.url_base + '%s'} + 'filter_cat[2]=1', 'action=basic', 'searchsubmit=1'])} self.url = self.urls['config_provider_home_uri'] diff --git a/sickbeard/providers/ncore.py b/sickbeard/providers/ncore.py index 0316fce0..11226e95 100644 --- a/sickbeard/providers/ncore.py +++ b/sickbeard/providers/ncore.py @@ -38,7 +38,7 @@ class NcoreProvider(generic.TorrentProvider): 'search': self.url_base + 'torrents.php?mire=%s&' + '&'.join([ 'miszerint=fid', 'hogyan=DESC', 'tipus=kivalasztottak_kozott', 'kivalasztott_tipus=xvidser,dvdser,hdser', 'miben=name']), - 'get': self.url_base + '%s'} + 'get': self.url_base + '%s&key='} self.url = self.urls['config_provider_home_uri'] @@ -59,7 +59,8 @@ class NcoreProvider(generic.TorrentProvider): items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} - rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'list': '.*?torrent_all', 'info': 'details'}.iteritems()) + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in { + 'list': '.*?torrent_all', 'info': 'details', 'key': 'key=([^"]+)">Torrent let'}.iteritems()) for mode in search_params.keys(): for search_string in search_params[mode]: search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string @@ -76,6 +77,7 @@ class NcoreProvider(generic.TorrentProvider): with BS4Parser(html, features=['html5lib', 'permissive']) as soup: torrent_table = soup.find('div', class_=rc['list']) torrent_rows = [] if not torrent_table else torrent_table.find_all('div', class_='box_torrent') + key = rc['key'].findall(html)[0] if not len(torrent_rows): raise generic.HaltParseException @@ -90,7 +92,7 @@ class NcoreProvider(generic.TorrentProvider): anchor = tr.find('a', href=rc['info']) title = (anchor.get('title') or anchor.get_text()).strip() - download_url = self._link(anchor.get('href').replace('details', 'download')) + download_url = self._link(anchor.get('href').replace('details', 'download')) + key except (AttributeError, TypeError, ValueError): continue diff --git a/sickbeard/providers/transmithe_net.py b/sickbeard/providers/nebulance.py similarity index 64% rename from sickbeard/providers/transmithe_net.py rename to sickbeard/providers/nebulance.py index 56e373f7..b6abcd87 100644 --- a/sickbeard/providers/transmithe_net.py +++ b/sickbeard/providers/nebulance.py @@ -25,12 +25,12 @@ from sickbeard.helpers import tryInt from lib.unidecode import unidecode -class TransmithenetProvider(generic.TorrentProvider): +class NebulanceProvider(generic.TorrentProvider): def __init__(self): - generic.TorrentProvider.__init__(self, 'Transmithe.net', cache_update_freq=17) + generic.TorrentProvider.__init__(self, 'Nebulance', cache_update_freq=17) - self.url_base = 'https://transmithe.net/' + self.url_base = 'https://nebulance.io/' self.urls = {'config_provider_home_uri': self.url_base, 'login_action': self.url_base + 'login.php', 'user': self.url_base + 'ajax.php?action=index', @@ -42,11 +42,11 @@ class TransmithenetProvider(generic.TorrentProvider): self.user_authkey, self.user_passkey = 2 * [None] self.chk_td = True - self.username, self.password, self.freeleech, self.minseed, self.minleech = 5 * [None] + self.username, self.password, self.freeleech, self.scene, self.minseed, self.minleech = 6 * [None] def _authorised(self, **kwargs): - if not super(TransmithenetProvider, self)._authorised( + if not super(NebulanceProvider, self)._authorised( logged_in=(lambda y=None: self.has_all_cookies('session')), post_params={'keeplogged': '1', 'form_tmpl': True}): return False @@ -77,7 +77,7 @@ class TransmithenetProvider(generic.TorrentProvider): cnt = len(items[mode]) try: - for item in data_json['response'].get('results', []): + for item in data_json.get('response', {}).get('results', []): if self.freeleech and not item.get('isFreeleech'): continue @@ -96,7 +96,7 @@ class TransmithenetProvider(generic.TorrentProvider): (maybe_res and [maybe_res[0]] or []) + [detail[0].strip(), detail[1], maybe_ext and maybe_ext[0].lower() or 'mkv'])) except (IndexError, KeyError): - title = group_name + title = self.regulate_title(item, group_name) download_url = self.urls['get'] % (self.user_authkey, self.user_passkey, torrent_id) if title and download_url: @@ -110,13 +110,52 @@ class TransmithenetProvider(generic.TorrentProvider): return results - def _season_strings(self, ep_obj, **kwargs): + @staticmethod + def regulate_title(item, t_param): - return generic.TorrentProvider._season_strings(self, ep_obj, scene=False) + if 'tags' not in item or not any(item['tags']): + return t_param - def _episode_strings(self, ep_obj, **kwargs): + t = [''] + bl = '[*\[({]+\s*' + br = '\s*[})\]*]+' + title = re.sub('(.*?)((?i)%sproper%s)(.*)' % (bl, br), r'\1\3\2', item['groupName']) + for r in '\s+-\s+', '(?:19|20)\d\d(?:\-\d\d\-\d\d)?', 'S\d\d+(?:E\d\d+)?': + m = re.findall('(.*%s)(.*)' % r, title) + if any(m) and len(m[0][0]) > len(t[0]): + t = m[0] + t = (tuple(title), t)[any(t)] - return generic.TorrentProvider._episode_strings(self, ep_obj, scene=False, **kwargs) + tag_str = '_'.join(item['tags']) + tags = [re.findall(x, tag_str, flags=re.X) for x in + ('(?i)%sProper%s|\bProper\b$' % (bl, br), + '(?i)\d{3,4}(?:[pi]|hd)', + ''' + (?i)(hr.ws.pdtv|blu.?ray|hddvd| + pdtv|hdtv|dsr|tvrip|web.?(?:dl|rip)|dvd.?rip|b[r|d]rip|mpeg-?2) + ''', ''' + (?i)([hx].?26[45]|divx|xvid) + ''', ''' + (?i)(avi|mkv|mp4|sub(?:b?ed|pack|s)) + ''')] + + title = ('%s`%s' % ( + re.sub('|'.join(['|'.join([re.escape(y) for y in x]) for x in tags if x]).strip('|'), '', t[-1]), + re.sub('(?i)(\d{3,4})hd', r'\1p', '`'.join(['`'.join(x) for x in tags[:-1]]).rstrip('`')) + + ('', '`hdtv')[not any(tags[2])] + ('', '`x264')[not any(tags[3])])) + for r in [('(?i)(?:\W(?:Series|Season))?\W(Repack)\W', r'`\1`'), + ('(?i)%s(Proper)%s' % (bl, br), r'`\1`'), ('%s\s*%s' % (bl, br), '`')]: + title = re.sub(r[0], r[1], title) + + grp = filter(lambda rn: '.release' in rn.lower(), item['tags']) + title = '%s%s-%s' % (('', t[0])[1 < len(t)], title, + (any(grp) and grp[0] or 'nogrp').upper().replace('.RELEASE', '')) + + for r in [('\s+[-]?\s+|\s+`|`\s+', '`'), ('`+', '.')]: + title = re.sub(r[0], r[1], title) + + title += + any(tags[4]) and ('.%s' % tags[4][0]) or '' + return title -provider = TransmithenetProvider() +provider = NebulanceProvider() diff --git a/sickbeard/providers/newznab.py b/sickbeard/providers/newznab.py index 4cb39b5a..1cf31c17 100755 --- a/sickbeard/providers/newznab.py +++ b/sickbeard/providers/newznab.py @@ -28,15 +28,16 @@ from math import ceil from sickbeard.sbdatetime import sbdatetime from . import generic from sickbeard import helpers, logger, scene_exceptions, tvcache, classes, db -from sickbeard.common import Quality +from sickbeard.common import neededQualities, Quality from sickbeard.exceptions import AuthException, MultipleShowObjectsException from sickbeard.indexers.indexer_config import * from io import BytesIO from lib.dateutil import parser from sickbeard.network_timezones import sb_timezone +from sickbeard.helpers import tryInt try: - from lxml import etree + from lxml import etree except ImportError: try: import xml.etree.cElementTree as etree @@ -55,14 +56,18 @@ class NewznabConstants: CAT_HEVC = -203 CAT_ANIME = -204 CAT_SPORT = -205 + CAT_WEBDL = -206 catSearchStrings = {r'^Anime$': CAT_ANIME, r'^Sport$': CAT_SPORT, r'^SD$': CAT_SD, + r'^BoxSD$': CAT_SD, r'^HD$': CAT_HD, + r'^BoxHD$': CAT_HD, r'^UHD$': CAT_UHD, r'^4K$': CAT_UHD, - r'^HEVC$': CAT_HEVC} + #r'^HEVC$': CAT_HEVC, + r'^WEB.?DL$': CAT_WEBDL} providerToIndexerMapping = {'tvdbid': INDEXER_TVDB, 'rageid': INDEXER_TVRAGE, @@ -90,17 +95,19 @@ class NewznabConstants: class NewznabProvider(generic.NZBProvider): def __init__(self, name, url, key='', cat_ids=None, search_mode=None, - search_fallback=False, enable_recentsearch=False, enable_backlog=False): + search_fallback=False, enable_recentsearch=False, enable_backlog=False, enable_scheduled_backlog=False): generic.NZBProvider.__init__(self, name, True, False) self.url = url self.key = key + self._exclude = set() self.cat_ids = cat_ids or '' self._cat_ids = None self.search_mode = search_mode or 'eponly' - self.search_fallback = search_fallback - self.enable_recentsearch = enable_recentsearch - self.enable_backlog = enable_backlog + self.search_fallback = bool(tryInt(search_fallback)) + self.enable_recentsearch = bool(tryInt(enable_recentsearch)) + self.enable_backlog = bool(tryInt(enable_backlog)) + self.enable_scheduled_backlog = bool(tryInt(enable_scheduled_backlog, 1)) self.needs_auth = '0' != self.key.strip() # '0' in the key setting indicates that api_key is not needed self.default = False self._caps = {} @@ -130,6 +137,11 @@ class NewznabProvider(generic.NZBProvider): self.check_cap_update() return self._caps_cats + @property + def excludes(self): + self.check_cap_update() + return self._exclude + @property def all_cats(self): self.check_cap_update() @@ -163,26 +175,38 @@ class NewznabProvider(generic.NZBProvider): self._last_recent_search = value def check_cap_update(self): - if not self._caps or (datetime.datetime.now() - self._caps_last_updated) >= datetime.timedelta(days=1): + if self.enabled and \ + (not self._caps or (datetime.datetime.now() - self._caps_last_updated) >= datetime.timedelta(days=1)): self.get_caps() def _get_caps_data(self): xml_caps = None - if datetime.date.today() - self._caps_need_apikey['date'] > datetime.timedelta(days=30) or \ - not self._caps_need_apikey['need']: - self._caps_need_apikey['need'] = False - data = self.get_url('%s/api?t=caps' % self.url) - if data: - xml_caps = helpers.parse_xml(data) - if (xml_caps is None or not hasattr(xml_caps, 'tag') or xml_caps.tag == 'error' or xml_caps.tag != 'caps') and \ - self.maybe_apikey(): - data = self.get_url('%s/api?t=caps&apikey=%s' % (self.url, self.maybe_apikey())) - if data: - xml_caps = helpers.parse_xml(data) - if xml_caps and hasattr(xml_caps, 'tag') and xml_caps.tag == 'caps': - self._caps_need_apikey = {'need': True, 'date': datetime.date.today()} + if self.enabled: + if datetime.date.today() - self._caps_need_apikey['date'] > datetime.timedelta(days=30) or \ + not self._caps_need_apikey['need']: + self._caps_need_apikey['need'] = False + data = self.get_url('%s/api?t=caps' % self.url) + if data: + xml_caps = helpers.parse_xml(data) + if xml_caps is None or not hasattr(xml_caps, 'tag') or xml_caps.tag == 'error' or xml_caps.tag != 'caps': + api_key = self.maybe_apikey() + if isinstance(api_key, basestring) and api_key not in ('0', ''): + data = self.get_url('%s/api?t=caps&apikey=%s' % (self.url, api_key)) + if data: + xml_caps = helpers.parse_xml(data) + if xml_caps and hasattr(xml_caps, 'tag') and xml_caps.tag == 'caps': + self._caps_need_apikey = {'need': True, 'date': datetime.date.today()} return xml_caps + def _check_excludes(self, cats): + if isinstance(cats, dict): + c = [] + for v in cats.itervalues(): + c.extend(v) + self._exclude = set(c) + else: + self._exclude = set(v for v in cats) + def get_caps(self): caps = {} cats = {} @@ -226,9 +250,11 @@ class NewznabProvider(generic.NZBProvider): logger.log('Error parsing result for [%s]' % self.name, logger.DEBUG) if not caps and self._caps and not all_cats and self._caps_all_cats and not cats and self._caps_cats: + self._check_excludes(cats) return - self._caps_last_updated = datetime.datetime.now() + if self.enabled: + self._caps_last_updated = datetime.datetime.now() if not caps and self.get_id() not in ['sick_beard_index']: caps[INDEXER_TVDB] = 'tvdbid' @@ -242,27 +268,26 @@ class NewznabProvider(generic.NZBProvider): caps[INDEXER_TVRAGE] = 'rid' if NewznabConstants.CAT_HD not in cats or not cats.get(NewznabConstants.CAT_HD): - cats[NewznabConstants.CAT_HD] = ['5040'] + cats[NewznabConstants.CAT_HD] = (['5040'], ['5040', '5090'])['nzbs_org' == self.get_id()] if NewznabConstants.CAT_SD not in cats or not cats.get(NewznabConstants.CAT_SD): - cats[NewznabConstants.CAT_SD] = ['5030'] + cats[NewznabConstants.CAT_SD] = (['5030'], ['5030', '5070'])['nzbs_org' == self.get_id()] if NewznabConstants.CAT_ANIME not in cats or not cats.get(NewznabConstants.CAT_ANIME): - cats[NewznabConstants.CAT_ANIME] = (['5070'], ['6070,7040'])['nzbs_org' == self.get_id()] + cats[NewznabConstants.CAT_ANIME] = (['5070'], ['6070', '7040'])['nzbs_org' == self.get_id()] if NewznabConstants.CAT_SPORT not in cats or not cats.get(NewznabConstants.CAT_SPORT): cats[NewznabConstants.CAT_SPORT] = ['5060'] + self._check_excludes(cats) self._caps = caps self._caps_cats = cats self._caps_all_cats = all_cats - @staticmethod - def clean_newznab_categories(cats): + def clean_newznab_categories(self, cats): """ - Removes the anime (5070), sports (5060), HD (5040), UHD (5045), SD (5030) categories from the list + Removes automatically mapped categories from the list """ - exclude = {'5070', '5060', '5040', '5045', '5030'} if isinstance(cats, list): - return [x for x in cats if x['id'] not in exclude] - return ','.join(set(cats.split(',')) - exclude) + return [x for x in cats if x['id'] not in self.excludes] + return ','.join(set(cats.split(',')) - self.excludes) def check_auth_from_data(self, data): @@ -286,15 +311,16 @@ class NewznabProvider(generic.NZBProvider): logger.WARNING) else: logger.log('Unknown error given from %s: %s' % (self.name, data.get('description', '')), - logger.ERROR) + logger.WARNING) return False return True def config_str(self): - return '%s|%s|%s|%s|%i|%s|%i|%i|%i' \ + return '%s|%s|%s|%s|%i|%s|%i|%i|%i|%i' \ % (self.name or '', self.url or '', self.maybe_apikey() or '', self.cat_ids or '', self.enabled, - self.search_mode or '', self.search_fallback, self.enable_recentsearch, self.enable_backlog) + self.search_mode or '', self.search_fallback, self.enable_recentsearch, self.enable_backlog, + self.enable_scheduled_backlog) def _season_strings(self, ep_obj): @@ -411,16 +437,29 @@ class NewznabProvider(generic.NZBProvider): def _title_and_url(self, item): title, url = None, None try: - title = item.findtext('title') - url = item.findtext('link') + title = ('%s' % item.findtext('title')).strip() + title = re.sub(r'\s+', '.', title) + # remove indexer specific release name parts + r_found = True + while r_found: + r_found = False + for pattern, repl in ((r'(?i)-Obfuscated$', ''), (r'(?i)-postbot$', '')): + if re.search(pattern, title): + r_found = True + title = re.sub(pattern, repl, title) + url = str(item.findtext('link')).replace('&', '&') except (StandardError, Exception): pass - title = title and re.sub(r'\s+', '.', '%s' % title) - url = url and str(url).replace('&', '&') - return title, url + def get_size_uid(self, item, **kwargs): + size = -1 + uid = None + if 'name_space' in kwargs and 'newznab' in kwargs['name_space']: + size, uid = self._parse_size_uid(item, kwargs['name_space']) + return size, uid + def get_show(self, item, **kwargs): show_obj = None if 'name_space' in kwargs and 'newznab' in kwargs['name_space']: @@ -435,36 +474,26 @@ class NewznabProvider(generic.NZBProvider): def choose_search_mode(self, episodes, ep_obj, hits_per_page=100): if not hasattr(ep_obj, 'eps_aired_in_season'): - return None, True, True, True, hits_per_page + return None, neededQualities(need_all_qualities=True), hits_per_page searches = [e for e in episodes if (not ep_obj.show.is_scene and e.season == ep_obj.season) or (ep_obj.show.is_scene and e.scene_season == ep_obj.scene_season)] - need_sd = need_hd = need_uhd = False - max_sd = Quality.SDDVD - hd_qualities = [Quality.HDTV, Quality.FULLHDTV, Quality.HDWEBDL, Quality.FULLHDWEBDL, - Quality.HDBLURAY, Quality.FULLHDBLURAY] - max_hd = Quality.FULLHDBLURAY + + needed = neededQualities() for s in searches: - if need_sd and need_hd and need_uhd: + if needed.all_qualities_needed: break if not s.show.is_anime and not s.show.is_sports: - if Quality.UNKNOWN in s.wantedQuality: - need_sd = need_hd = need_uhd = True - else: - if not need_sd and min(s.wantedQuality) <= max_sd: - need_sd = True - if not need_hd and any(i in hd_qualities for i in s.wantedQuality): - need_hd = True - if not need_uhd and max(s.wantedQuality) > max_hd: - need_uhd = True + needed.check_needed_qualities(s.wantedQuality) + per_ep, limit_per_ep = 0, 0 - if need_sd and not need_hd: + if needed.need_sd and not needed.need_hd: per_ep, limit_per_ep = 10, 25 - if need_hd: - if not need_sd: + if needed.need_hd: + if not needed.need_sd: per_ep, limit_per_ep = 30, 90 else: per_ep, limit_per_ep = 40, 120 - if need_uhd or (need_hd and not self.cats.get(NewznabConstants.CAT_UHD)): + if needed.need_uhd or (needed.need_hd and not self.cats.get(NewznabConstants.CAT_UHD)): per_ep += 4 limit_per_ep += 10 if ep_obj.show.is_anime or ep_obj.show.is_sports or ep_obj.show.air_by_date: @@ -477,18 +506,10 @@ class NewznabProvider(generic.NZBProvider): ep_obj.eps_aired_in_season * limit_per_ep) / hits_per_page)) season_search = rel < (len(searches) * 100 // hits_per_page) if not season_search: - need_sd = need_hd = need_uhd = False + needed = neededQualities() if not ep_obj.show.is_anime and not ep_obj.show.is_sports: - if Quality.UNKNOWN in ep_obj.wantedQuality: - need_sd = need_hd = need_uhd = True - else: - if min(ep_obj.wantedQuality) <= max_sd: - need_sd = True - if any(i in hd_qualities for i in ep_obj.wantedQuality): - need_hd = True - if max(ep_obj.wantedQuality) > max_hd: - need_uhd = True - return (season_search, need_sd, need_hd, need_uhd, + needed.check_needed_qualities(ep_obj.wantedQuality) + return (season_search, needed, (hits_per_page * 100 // hits_per_page * 2, hits_per_page * int(ceil(rel_limit * 1.5)))[season_search]) def find_search_results(self, show, episodes, search_mode, manual_search=False, try_other_searches=False, **kwargs): @@ -517,8 +538,8 @@ class NewznabProvider(generic.NZBProvider): # found result, search next episode continue - s_mode, need_sd, need_hd, need_uhd, max_items = self.choose_search_mode( - episodes, ep_obj, hits_per_page=self.limits) + s_mode, needed, max_items = self.choose_search_mode(episodes, ep_obj, hits_per_page=self.limits) + needed.check_needed_types(self.show) if 'sponly' == search_mode: searched_scene_season = ep_obj.scene_season @@ -535,9 +556,8 @@ class NewznabProvider(generic.NZBProvider): for cur_param in search_params: items, n_space = self._search_provider(cur_param, search_mode=search_mode, epcount=len(episodes), - need_anime=self.show.is_anime, need_sports=self.show.is_sports, - need_sd=need_sd, need_hd=need_hd, need_uhd=need_uhd, - max_items=max_items, try_all_searches=try_other_searches) + needed=needed, max_items=max_items, + try_all_searches=try_other_searches) item_list += items name_space.update(n_space) @@ -562,8 +582,23 @@ class NewznabProvider(generic.NZBProvider): return parsed_date - def _search_provider(self, search_params, need_anime=True, need_sports=True, need_sd=True, need_hd=True, - need_uhd=True, max_items=400, try_all_searches=False, **kwargs): + @staticmethod + def _parse_size_uid(item, ns, default=-1): + parsed_size = default + uid = None + try: + if ns and 'newznab' in ns: + for attr in item.findall('%sattr' % ns['newznab']): + if 'size' == attr.get('name', ''): + parsed_size = helpers.tryInt(attr.get('value'), -1) + elif 'guid' == attr.get('name', ''): + uid = attr.get('value') + except (StandardError, Exception): + pass + return parsed_size, uid + + def _search_provider(self, search_params, needed=neededQualities(need_all=True), max_items=400, + try_all_searches=False, **kwargs): api_key = self._check_auth() @@ -574,7 +609,7 @@ class NewznabProvider(generic.NZBProvider): if v in self.caps]), 'offset': 0} - if isinstance(api_key, basestring): + if isinstance(api_key, basestring) and api_key not in ('0', ''): base_params['apikey'] = api_key results, n_spaces = [], {} @@ -585,6 +620,7 @@ class NewznabProvider(generic.NZBProvider): cat_hd = self.cats.get(NewznabConstants.CAT_HD, ['5040']) cat_sd = self.cats.get(NewznabConstants.CAT_SD, ['5030']) cat_uhd = self.cats.get(NewznabConstants.CAT_UHD) + cat_webdl = self.cats.get(NewznabConstants.CAT_WEBDL) for mode in search_params.keys(): for i, params in enumerate(search_params[mode]): @@ -598,17 +634,19 @@ class NewznabProvider(generic.NZBProvider): logger.log('Show is missing either an id or search term for search') continue - if need_anime: + if needed.need_anime: cat.extend(cat_anime) - if need_sports: + if needed.need_sports: cat.extend(cat_sport) - if need_hd: + if needed.need_hd: cat.extend(cat_hd) - if need_sd: + if needed.need_sd: cat.extend(cat_sd) - if need_uhd and cat_uhd is not None: + if needed.need_uhd and cat_uhd is not None: cat.extend(cat_uhd) + if needed.need_webdl and cat_webdl is not None: + cat.extend(cat_webdl) if self.cat_ids or len(cat): base_params['cat'] = ','.join(sorted(set((self.cat_ids.split(',') if self.cat_ids else []) + cat))) @@ -636,7 +674,7 @@ class NewznabProvider(generic.NZBProvider): data = helpers.getURL(search_url) if not data: - logger.log('No Data returned from %s' % self.name, logger.DEBUG) + logger.log('No Data returned from %s' % self.name, logger.WARNING) break # hack this in until it's fixed server side @@ -647,14 +685,14 @@ class NewznabProvider(generic.NZBProvider): parsed_xml, n_spaces = self.cache.parse_and_get_ns(data) items = parsed_xml.findall('channel/item') except (StandardError, Exception): - logger.log('Error trying to load %s RSS feed' % self.name, logger.ERROR) + logger.log('Error trying to load %s RSS feed' % self.name, logger.WARNING) break if not self.check_auth_from_data(parsed_xml): break if 'rss' != parsed_xml.tag: - logger.log('Resulting XML from %s isn\'t RSS, not parsing it' % self.name, logger.ERROR) + logger.log('Resulting XML from %s isn\'t RSS, not parsing it' % self.name, logger.WARNING) break i and time.sleep(2.1) @@ -735,8 +773,8 @@ class NewznabProvider(generic.NZBProvider): search_terms = [] regex = [] if shows: - search_terms += ['.proper.', '.repack.'] - regex += ['proper|repack'] + search_terms += ['.proper.', '.repack.', '.real.'] + regex += ['proper|repack', Quality.real_check] proper_check = re.compile(r'(?i)(\b%s\b)' % '|'.join(regex)) if anime: terms = 'v1|v2|v3|v4|v5' @@ -773,9 +811,11 @@ class NewznabProvider(generic.NZBProvider): logger.log(u'Unable to figure out the date for entry %s, skipping it' % title) continue + result_size, result_uid = self._parse_size_uid(item, ns=n_space) if not search_date or search_date < result_date: show_obj = self.get_show(item, name_space=n_space) - search_result = classes.Proper(title, url, result_date, self.show, parsed_show=show_obj) + search_result = classes.Proper(title, url, result_date, self.show, parsed_show=show_obj, + size=result_size, puid=result_uid) results.append(search_result) time.sleep(0.5) @@ -810,7 +850,7 @@ class NewznabCache(tvcache.TVCache): root = elem return root, ns - def updateCache(self, need_anime=True, need_sports=True, need_sd=True, need_hd=True, need_uhd=True, **kwargs): + def updateCache(self, needed=neededQualities(need_all=True), **kwargs): result = [] @@ -818,8 +858,7 @@ class NewznabCache(tvcache.TVCache): n_spaces = {} try: self._checkAuth() - (items, n_spaces) = self.provider.cache_data(need_anime=need_anime, need_sports=need_sports, - need_sd=need_sd, need_hd=need_hd, need_uhd=need_uhd) + (items, n_spaces) = self.provider.cache_data(needed=needed) except (StandardError, Exception): items = None @@ -856,19 +895,14 @@ class NewznabCache(tvcache.TVCache): # overwrite method with that parses the rageid from the newznab feed def _parseItem(self, ns, item): - title = item.findtext('title') - url = item.findtext('link') + title, url = self._title_and_url(item) ids = self.parse_ids(item, ns) - self._checkItemAuth(title, url) - if not title or not url: logger.log('The data returned from the %s feed is incomplete, this result is unusable' % self.provider.name, logger.DEBUG) return None - url = self._translateLinkURL(url) - logger.log('Attempting to add item from RSS to cache: %s' % title, logger.DEBUG) return self.add_cache_entry(title, url, id_dict=ids) diff --git a/sickbeard/providers/extratorrent.py b/sickbeard/providers/nyaa.py similarity index 67% rename from sickbeard/providers/extratorrent.py rename to sickbeard/providers/nyaa.py index b4cee880..aae8184d 100644 --- a/sickbeard/providers/extratorrent.py +++ b/sickbeard/providers/nyaa.py @@ -1,114 +1,101 @@ -# coding=utf-8 -# -# This file is part of SickGear. -# -# SickGear is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SickGear is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SickGear. If not, see . - -import re -import traceback -import urllib - -from . import generic -from sickbeard import logger -from sickbeard.bs4_parser import BS4Parser -from sickbeard.helpers import tryInt -from lib.unidecode import unidecode - - -class ExtraTorrentProvider(generic.TorrentProvider): - - def __init__(self): - generic.TorrentProvider.__init__(self, 'ExtraTorrent') - - self.url_home = ['https://www.extratorrent%s/' % u for u in '.life', 'live.com', 'online.com', '.one'] + \ - ['https://extra.to/'] + \ - ['https://etmirror.com/', 'https://etproxy.com/', 'https://extratorrent.usbypass.xyz/'] - - self.url_vars = {'search': 'search/?new=1&search=%s&s_cat=8', 'browse': 'view/today/TV.html', - 'get': '%s'} - self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'search': '%(home)s%(vars)s', - 'browse': '%(home)s%(vars)s', 'get': '%(home)s%(vars)s'} - - self.minseed, self.minleech = 2 * [None] - - @staticmethod - def _has_signature(data=None): - return data and re.search(r'(?i)ExtraTorrent', data[33:1024:]) - - def _search_provider(self, search_params, **kwargs): - - results = [] - if not self.url: - return results - - items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} - - rc = dict((k, re.compile('(?i)' + v)) for (k, v) in { - 'get': 'download', 'title': '(?:^download|torrent$)', 'get_url': '^/(torrent_)?'}.iteritems()) - - for mode in search_params.keys(): - for search_string in search_params[mode]: - - search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string - - search_url = self.urls['browse'] if 'Cache' == mode \ - else self.urls['search'] % (urllib.quote_plus(search_string)) - - html = self.get_url(search_url) - - cnt = len(items[mode]) - try: - if not html or self._has_no_results(html): - raise generic.HaltParseException - with BS4Parser(html, features=['html5lib', 'permissive']) as soup: - torrent_table = soup.find('table', class_='tl') - torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') - - if 2 > len(torrent_rows): - raise generic.HaltParseException - - head = None - for tr in torrent_rows[1:]: - cells = tr.find_all('td') - if 5 > len(cells): - continue - try: - head = head if None is not head else self._header_row(tr) - seeders, leechers, size = [tryInt(n.replace('---', '0'), n) for n in [ - cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] - if self._peers_fail(mode, seeders, leechers): - continue - - info = tr.find('a', title=rc['get']) or {} - title = rc['title'].sub('', info.get('title') or '').strip() - download_url = self._link(rc['get_url'].sub('', info['href'])) - except (AttributeError, TypeError, ValueError, IndexError): - continue - - if title and download_url: - items[mode].append((title, download_url, seeders, self._bytesizer(size))) - - except generic.HaltParseException: - pass - except (StandardError, Exception): - logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) - - self._log_search(mode, len(items[mode]) - cnt, search_url) - - results = self._sort_seeding(mode, results + items[mode]) - - return results - - -provider = ExtraTorrentProvider() +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import re +import traceback + +from . import generic +from sickbeard import logger, show_name_helpers +from sickbeard.bs4_parser import BS4Parser +from sickbeard.helpers import tryInt +from lib.unidecode import unidecode + + +class NyaaProvider(generic.TorrentProvider): + + def __init__(self): + generic.TorrentProvider.__init__(self, 'Nyaa', anime_only=True) + + self.url_base = 'https://www.nyaa.si/' + self.urls = {'config_provider_home_uri': self.url_base, 'search': self.url_base + '?f=%s&c=1_0&q=%s'} + + self.url = self.urls['config_provider_home_uri'] + + self.minseed, self.minleech = 2 * [None] + self.confirmed = False + + def _search_provider(self, search_params, **kwargs): + + results = [] + + if self.show and not self.show.is_anime: + return results + + items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} + + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'info': 'view', 'get': '(?:torrent|magnet:)'}.items()) + for mode in search_params.keys(): + for search_string in search_params[mode]: + search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string + search_url = self.urls['search'] % ((0, 2)[self.confirmed], search_string) + + html = self.get_url(search_url) + + cnt = len(items[mode]) + try: + if not html or self._has_no_results(html): + raise generic.HaltParseException + + with BS4Parser(html, features=['html5lib', 'permissive']) as soup: + torrent_table = soup.find('table', class_='torrent-list') + torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') + + if 2 > len(torrent_rows): + raise generic.HaltParseException + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + try: + head = head if None is not head else self._header_row(tr) + seeders, leechers, size = [tryInt(n, n) for n in [ + cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] + if self._peers_fail(mode, seeders, leechers): + continue + + title = tr.find('a', href=rc['info']).get_text().strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) + except (AttributeError, TypeError, ValueError, IndexError): + continue + + if title and download_url: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(items[mode]) - cnt, search_url) + + results = self._sort_seeding(mode, results + items[mode]) + + return results + + +provider = NyaaProvider() diff --git a/sickbeard/providers/nyaatorrents.py b/sickbeard/providers/nyaatorrents.py deleted file mode 100644 index 24a6f951..00000000 --- a/sickbeard/providers/nyaatorrents.py +++ /dev/null @@ -1,107 +0,0 @@ -# -# This file is part of SickGear. -# -# SickGear is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SickGear is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SickGear. If not, see . - -import re -import urllib - -from . import generic -from sickbeard import logger, show_name_helpers, tvcache -from sickbeard.helpers import tryInt - - -class NyaaProvider(generic.TorrentProvider): - - def __init__(self): - generic.TorrentProvider.__init__(self, 'NyaaTorrents', anime_only=True) - - self.url_base = self.url = 'https://www.nyaa.se/' - - self.minseed, self.minleech = 2 * [None] - - self.cache = NyaaCache(self) - - def _search_provider(self, search_string, search_mode='eponly', **kwargs): - - if self.show and not self.show.is_anime: - return [] - - params = urllib.urlencode({'term': search_string.encode('utf-8'), - 'cats': '1_37', # Limit to English-translated Anime (for now) - # 'sort': '2', # Sort Descending By Seeders - }) - - return self.get_data(getrss_func=self.cache.getRSSFeed, - search_url='%s?page=rss&%s' % (self.url, params), - mode=('Episode', 'Season')['sponly' == search_mode]) - - def get_data(self, getrss_func, search_url, mode='cache'): - - data = getrss_func(search_url) - - results = [] - if data and 'entries' in data: - - rc = dict((k, re.compile('(?i)' + v)) for (k, v) in { - 'stats': '(\d+)\W+seed[^\d]+(\d+)\W+leech[^\d]+\d+\W+down[^\d]+([\d.,]+\s\w+)'}.iteritems()) - - for cur_item in data.get('entries', []): - try: - seeders, leechers, size = 0, 0, 0 - stats = rc['stats'].findall(cur_item.get('summary_detail', {'value': ''}).get('value', '')) - if len(stats): - seeders, leechers, size = (tryInt(n, n) for n in stats[0]) - if self._peers_fail(mode, seeders, leechers): - continue - title, download_url = self._title_and_url(cur_item) - download_url = self._link(download_url) - except (AttributeError, TypeError, ValueError, IndexError): - continue - - if title and download_url: - results.append((title, download_url, seeders, self._bytesizer(size))) - - self._log_search(mode, len(results), search_url) - - return self._sort_seeding(mode, results) - - def _season_strings(self, ep_obj, **kwargs): - - return show_name_helpers.makeSceneShowSearchStrings(self.show) - - def _episode_strings(self, ep_obj, **kwargs): - - return self._season_strings(ep_obj) - - -class NyaaCache(tvcache.TVCache): - - def __init__(self, this_provider): - tvcache.TVCache.__init__(self, this_provider) - - self.update_freq = 15 - - def _cache_data(self): - - params = urllib.urlencode({'page': 'rss', # Use RSS page - 'order': '1', # Sort Descending By Date - 'cats': '1_37' # Limit to English-translated Anime (for now) - }) - - return self.provider.get_data(getrss_func=self.getRSSFeed, - search_url='%s?%s' % (self.provider.url, params)) - - -provider = NyaaProvider() diff --git a/sickbeard/providers/omgwtfnzbs.py b/sickbeard/providers/omgwtfnzbs.py index 27739a85..ebcd45b2 100644 --- a/sickbeard/providers/omgwtfnzbs.py +++ b/sickbeard/providers/omgwtfnzbs.py @@ -29,6 +29,7 @@ from sickbeard import classes, logger, show_name_helpers, tvcache from sickbeard.bs4_parser import BS4Parser from sickbeard.exceptions import AuthException from sickbeard.rssfeeds import RSSFeeds +from sickbeard.common import neededQualities class OmgwtfnzbsProvider(generic.NZBProvider): @@ -43,7 +44,6 @@ class OmgwtfnzbsProvider(generic.NZBProvider): self.urls = {'config_provider_home_uri': self.url_base, 'cache': 'https://rss.omgwtfnzbs.me/rss-download.php?%s', 'search': self.url_api + 'json/?%s', - 'get': self.url_base + '%s', 'cache_html': self.url_base + 'browse.php?cat=tv%s', 'search_html': self.url_base + 'browse.php?cat=tv&search=%s'} @@ -51,6 +51,10 @@ class OmgwtfnzbsProvider(generic.NZBProvider): self.username, self.api_key, self.cookies = 3 * [None] self.cache = OmgwtfnzbsCache(self) + cat_sd = ['19'] + cat_hd = ['20'] + cat_uhd = ['30'] + def _check_auth_from_data(self, parsed_data, is_xml=True): if parsed_data is None: @@ -92,21 +96,26 @@ class OmgwtfnzbsProvider(generic.NZBProvider): return item['release'].replace('_', '.'), item['getnzb'] + def get_data(self, url): + result = None + if url and False is self._init_api(): + data = self.get_url(url, timeout=90) + if data: + if re.search('(?i)limit.*?reached', data): + logger.log('Daily Nzb Download limit reached', logger.DEBUG) + elif '' not in data or 'seem to be logged in' in data: + logger.log('Failed nzb data response: %s' % data, logger.DEBUG) + else: + result = data + return result + def get_result(self, episodes, url): result = None if url and False is self._init_api(): - data = self.get_url(url, timeout=90) - if not data: - return result - if 'Limit Reached' in data: - logger.log('Daily Nzb Download limit reached', logger.DEBUG) - return result - if '' not in data or 'seem to be logged in' in data: - logger.log('Failed nzb data response: %s' % data, logger.DEBUG) - return result result = classes.NZBDataSearchResult(episodes) - result.extraInfo += [data] + result.get_data_func = self.get_data + result.url = url if None is result: result = classes.NZBSearchResult(episodes) @@ -116,16 +125,27 @@ class OmgwtfnzbsProvider(generic.NZBProvider): return result - def cache_data(self): + def _get_cats(self, needed): + cats = [] + if needed.need_sd: + cats.extend(OmgwtfnzbsProvider.cat_sd) + if needed.need_hd: + cats.extend(OmgwtfnzbsProvider.cat_hd) + if needed.need_uhd: + cats.extend(OmgwtfnzbsProvider.cat_uhd) + return cats + + def cache_data(self, needed=neededQualities(need_all=True), **kwargs): api_key = self._init_api() if False is api_key: - return self.search_html() + return self.search_html(needed=needed, **kwargs) + cats = self._get_cats(needed=needed) if None is not api_key: params = {'user': self.username, 'api': api_key, 'eng': 1, - 'catid': '19,20'} # SD,HD + 'catid': ','.join(cats)} # SD,HD rss_url = self.urls['cache'] % urllib.urlencode(params) @@ -136,18 +156,20 @@ class OmgwtfnzbsProvider(generic.NZBProvider): return data.entries return [] - def _search_provider(self, search, search_mode='eponly', epcount=0, retention=0, **kwargs): + def _search_provider(self, search, search_mode='eponly', epcount=0, retention=0, + needed=neededQualities(need_all=True), **kwargs): api_key = self._init_api() if False is api_key: - return self.search_html(search, search_mode) + return self.search_html(search, search_mode, needed=needed, **kwargs) results = [] + cats = self._get_cats(needed=needed) if None is not api_key: params = {'user': self.username, 'api': api_key, 'eng': 1, 'nukes': 1, - 'catid': '19,20', # SD,HD + 'catid': ','.join(cats), # SD,HD 'retention': (sickbeard.USENET_RETENTION, retention)[retention or not sickbeard.USENET_RETENTION], 'search': search} @@ -163,14 +185,16 @@ class OmgwtfnzbsProvider(generic.NZBProvider): results.append(item) return results - def search_html(self, search='', search_mode=''): + def search_html(self, search='', search_mode='', needed=neededQualities(need_all=True), **kwargs): results = [] if None is self.cookies: return results + cats = self._get_cats(needed=needed) + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'info': 'detail', 'get': r'send\?', 'nuked': r'\bnuked', - 'cat': 'cat=(?:19|20)'}.items()) + 'cat': 'cat=(?:%s)' % '|'.join(cats)}.items()) mode = ('search', 'cache')['' == search] search_url = self.urls[mode + '_html'] % search html = self.get_url(search_url) @@ -193,14 +217,14 @@ class OmgwtfnzbsProvider(generic.NZBProvider): if tr.find('img', src=rc['nuked']) or not tr.find('a', href=rc['cat']): continue - title = tr.find('a', href=rc['info'])['title'] + title = tr.find('a', href=rc['info']).get_text().strip() download_url = tr.find('a', href=rc['get']) age = tr.find_all('td')[-1]['data-sort'] except (AttributeError, TypeError, ValueError): continue if title and download_url and age: - results.append({'release': title, 'getnzb': self.urls['get'] % download_url['href'].lstrip('/'), + results.append({'release': title, 'getnzb': self._link(download_url['href']), 'usenetage': int(age.strip())}) except generic.HaltParseException: @@ -215,7 +239,7 @@ class OmgwtfnzbsProvider(generic.NZBProvider): def find_propers(self, **kwargs): - search_terms = ['.PROPER.', '.REPACK.'] + search_terms = ['.PROPER.', '.REPACK.', '.REAL.'] results = [] for term in search_terms: @@ -263,9 +287,9 @@ class OmgwtfnzbsCache(tvcache.TVCache): self.update_freq = 20 - def _cache_data(self): + def _cache_data(self, **kwargs): - return self.provider.cache_data() + return self.provider.cache_data(**kwargs) provider = OmgwtfnzbsProvider() diff --git a/sickbeard/providers/pisexy.py b/sickbeard/providers/pisexy.py index 248adf74..62cddb21 100644 --- a/sickbeard/providers/pisexy.py +++ b/sickbeard/providers/pisexy.py @@ -31,8 +31,7 @@ class PiSexyProvider(generic.TorrentProvider): self.url_base = 'https://pisexy.me/' self.urls = {'config_provider_home_uri': self.url_base, 'login': self.url_base + 'takelogin.php', - 'search': self.url_base + 'browseall.php?search=%s', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browseall.php?search=%s'} self.url = self.urls['config_provider_home_uri'] diff --git a/sickbeard/providers/ptf.py b/sickbeard/providers/ptf.py index 7d4145fb..fe741ffc 100644 --- a/sickbeard/providers/ptf.py +++ b/sickbeard/providers/ptf.py @@ -38,8 +38,7 @@ class PTFProvider(generic.TorrentProvider): self.url_base = 'https://ptfiles.net/' self.urls = {'config_provider_home_uri': self.url_base, 'login': self.url_base + 'panel.php?tool=links', - 'search': self.url_base + 'browse.php?search=%s&%s&incldead=0&title=0', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php?search=%s&%s&incldead=0&title=0'} self.categories = {'Season': [39], 'Episode': [7, 33, 42], 'anime': [23]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] diff --git a/sickbeard/providers/rarbg.py b/sickbeard/providers/rarbg.py index c62713dd..c3d98a47 100644 --- a/sickbeard/providers/rarbg.py +++ b/sickbeard/providers/rarbg.py @@ -30,7 +30,7 @@ class RarbgProvider(generic.TorrentProvider): def __init__(self): generic.TorrentProvider.__init__(self, 'Rarbg') - self.url_base = 'https://rarbg.unblocked.uno/' + self.url_base = 'https://rarbgmirror.xyz/' # api_spec: https://rarbg.com/pubapi/apidocs_v2.txt self.url_api = 'https://torrentapi.org/pubapi_v2.php?app_id=SickGear&' self.urls = {'config_provider_home_uri': self.url_base, @@ -168,7 +168,7 @@ class RarbgProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - search_params = generic.TorrentProvider._episode_strings(self, ep_obj, detail_only=True, date_or=True, **kwargs) + search_params = super(RarbgProvider, self)._episode_strings(ep_obj, detail_only=True, date_or=True, **kwargs) if self.show.air_by_date and self.show.is_sports: for x, types in enumerate(search_params): for y, ep_type in enumerate(types): diff --git a/sickbeard/providers/revtt.py b/sickbeard/providers/revtt.py index 0cfabc6a..e6d86c5a 100644 --- a/sickbeard/providers/revtt.py +++ b/sickbeard/providers/revtt.py @@ -33,8 +33,7 @@ class RevTTProvider(generic.TorrentProvider): self.url_base = 'https://revolutiontt.me/' self.urls = {'config_provider_home_uri': self.url_base, 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'browse.php?search=%s&%s&titleonly=1&incldead=0', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php?search=%s&%s&titleonly=1&incldead=0'} self.categories = {'Season': [43, 45], 'Episode': [7, 41, 42], 'anime': [23]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] diff --git a/sickbeard/providers/rsstorrent.py b/sickbeard/providers/rsstorrent.py index 8d3fea3b..6eace767 100644 --- a/sickbeard/providers/rsstorrent.py +++ b/sickbeard/providers/rsstorrent.py @@ -28,8 +28,9 @@ from lib.bencode import bdecode class TorrentRssProvider(generic.TorrentProvider): def __init__(self, name, url, cookies='', search_mode='eponly', search_fallback=False, - enable_recentsearch=False, enable_backlog=False): + enable_recentsearch=False, enable_backlog=False, enable_scheduled_backlog=False): self.enable_backlog = bool(tryInt(enable_backlog)) + self.enable_scheduled_backlog = bool(tryInt(enable_scheduled_backlog)) generic.TorrentProvider.__init__(self, name, supports_backlog=self.enable_backlog, cache_update_freq=15) self.url = url.rstrip('/') @@ -48,9 +49,10 @@ class TorrentRssProvider(generic.TorrentProvider): def config_str(self): - return '%s|%s|%s|%d|%s|%d|%d|%d' % ( + return '%s|%s|%s|%d|%s|%d|%d|%d|%d' % ( self.name or '', self.url or '', self.cookies or '', self.enabled, - self.search_mode or '', self.search_fallback, self.enable_recentsearch, self.enable_backlog) + self.search_mode or '', self.search_fallback, self.enable_recentsearch, self.enable_backlog, + self.enable_scheduled_backlog) def _title_and_url(self, item): @@ -88,7 +90,15 @@ class TorrentRssProvider(generic.TorrentProvider): if not (title and url): continue if url.startswith('magnet:'): - if re.search('urn:btih:([0-9a-f]{32,40})', url): + btih = None + try: + btih = re.findall('urn:btih:([\w]{32,40})', url)[0] + if 32 == len(btih): + from base64 import b16encode, b32decode + btih = b16encode(b32decode(btih)) + except (StandardError, Exception): + pass + if re.search('(?i)[0-9a-f]{32,40}', btih): break else: torrent_file = self.get_url(url) diff --git a/sickbeard/providers/scc.py b/sickbeard/providers/scc.py deleted file mode 100644 index df6aa952..00000000 --- a/sickbeard/providers/scc.py +++ /dev/null @@ -1,117 +0,0 @@ -# coding=utf-8 -# -# This file is part of SickGear. -# -# SickGear is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SickGear is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SickGear. If not, see . - -import re -import time -import traceback - -from . import generic -from sickbeard import logger -from sickbeard.bs4_parser import BS4Parser -from sickbeard.helpers import tryInt -from lib.unidecode import unidecode - - -class SCCProvider(generic.TorrentProvider): - - def __init__(self): - generic.TorrentProvider.__init__(self, 'SceneAccess') - - self.url_home = ['https://sceneaccess.%s/' % u for u in 'eu', 'org'] - - self.url_vars = { - 'login_action': 'login', 'search': 'browse?search=%s&method=1&c27=27&c17=17&c11=11', 'get': '%s', - 'nonscene': 'nonscene?search=%s&method=1&c44=44&c45=44', 'archive': 'archive?search=%s&method=1&c26=26'} - self.url_tmpl = { - 'config_provider_home_uri': '%(home)s', 'login_action': '%(home)s%(vars)s', 'search': '%(home)s%(vars)s', - 'get': '%(home)s%(vars)s', 'nonscene': '%(home)s%(vars)s', 'archive': '%(home)s%(vars)s'} - - self.username, self.password, self.minseed, self.minleech = 4 * [None] - - def _authorised(self, **kwargs): - - return super(SCCProvider, self)._authorised(post_params={'form_tmpl': 'method'}) - - def _search_provider(self, search_params, **kwargs): - - results = [] - items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} - - if not self._authorised(): - return results - - rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'info': 'detail', 'get': 'download'}.items()) - for mode in search_params.keys(): - for search_string in search_params[mode]: - search_string, void = self._title_and_url((search_string, None)) - search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string - - if 'Season' == mode: - searches = [self.urls['archive'] % search_string] - else: - searches = [self.urls['search'] % search_string, - self.urls['nonscene'] % search_string] - - for search_url in searches: - - html = self.get_url(search_url) - - cnt = len(items[mode]) - try: - if not html or self._has_no_results(html): - raise generic.HaltParseException - - with BS4Parser(html, features=['html5lib', 'permissive']) as soup: - torrent_table = soup.find(id='torrents-table') - torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') - - if 2 > len(torrent_rows): - raise generic.HaltParseException - - for tr in torrent_table.find_all('tr')[1:]: - try: - seeders, leechers, size = [tryInt(n, n) for n in [ - tr.find('td', class_='ttr_' + x).get_text().strip() - for x in 'seeders', 'leechers', 'size']] - if self._peers_fail(mode, seeders, leechers): - continue - - info = tr.find('a', href=rc['info']) - title = (info.attrs.get('title') or info.get_text()).strip() - download_url = self._link(tr.find('a', href=rc['get'])['href']) - except (AttributeError, TypeError, ValueError): - continue - - if title and download_url: - items[mode].append((title, download_url, seeders, self._bytesizer(size))) - - except generic.HaltParseException: - time.sleep(1.1) - except (StandardError, Exception): - logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) - self._log_search(mode, len(items[mode]) - cnt, search_url) - - results = self._sort_seeding(mode, results + items[mode]) - - return results - - def _episode_strings(self, ep_obj, **kwargs): - - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='.', **kwargs) - - -provider = SCCProvider() diff --git a/sickbeard/providers/scenehd.py b/sickbeard/providers/scenehd.py new file mode 100644 index 00000000..960dfb9c --- /dev/null +++ b/sickbeard/providers/scenehd.py @@ -0,0 +1,120 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import re +import traceback + +from . import generic +from sickbeard import logger +from sickbeard.bs4_parser import BS4Parser +from sickbeard.helpers import tryInt +from lib.unidecode import unidecode + + +class SceneHDProvider(generic.TorrentProvider): + + def __init__(self): + generic.TorrentProvider.__init__(self, 'SceneHD', cache_update_freq=20) + + self.url_home = ['https://scenehd.org/'] + + self.url_vars = {'login_action': 'login.php', 'search': 'browse.php?search=%s&cat=%s&sort=5'} + self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login_action': '%(home)s%(vars)s', + 'search': '%(home)s%(vars)s'} + + self.categories = {'shows': [5, 6, 7]} + + self.username, self.password, self.freeleech, self.minseed, self.minleech = 5 * [None] + self.confirmed = False + + def _authorised(self, **kwargs): + + return super(SceneHDProvider, self)._authorised(post_params={'form_tmpl': True}) + + def _search_provider(self, search_params, **kwargs): + + results = [] + if not self._authorised(): + return results + + items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} + + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'info': 'detail', 'get': 'download', + 'nuked': 'nuke', 'filter': 'free'}.items()) + for mode in search_params.keys(): + for search_string in search_params[mode]: + search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string + search_url = self.urls['search'] % (search_string, self._categories_string(mode, '%s', ',')) + + html = self.get_url(search_url, timeout=90) + + cnt = len(items[mode]) + try: + if not html or self._has_no_results(html): + raise generic.HaltParseException + + with BS4Parser(html, features=['html5lib', 'permissive'], attr='cellpadding="5"') as soup: + torrent_table = soup.find('table', class_='browse') + torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') + + if 2 > len(torrent_rows): + raise generic.HaltParseException + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + try: + info = tr.find('a', href=rc['info']) + head = head if None is not head else self._header_row(tr) + seeders, leechers, size = [n for n in [ + cells[head[x]].get_text().strip() for x in 'leech', 'leech', 'size']] + seeders, leechers, size = [tryInt(n, n) for n in + list(re.findall('^(\d+)[^\d]+?(\d+)', leechers)[0]) + + re.findall('^[^\n\t]+', size)] + if (self.confirmed and + any([tr.find('img', alt=rc['nuked']), tr.find('img', class_=rc['nuked'])])) \ + or (self.freeleech and not tr.find('a', class_=rc['filter'])) \ + or self._peers_fail(mode, seeders, leechers): + continue + + title = (info.attrs.get('title') or info.get_text()).strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) + except (AttributeError, TypeError, ValueError, KeyError): + continue + + if title and download_url: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(items[mode]) - cnt, search_url) + + results = self._sort_seeding(mode, results + items[mode]) + + return results + + @staticmethod + def ui_string(key): + return 'scenehd_confirm' == key and 'skip releases marked as bad/nuked' or '' + + +provider = SceneHDProvider() diff --git a/sickbeard/providers/shazbat.py b/sickbeard/providers/shazbat.py index a7dff22e..e235b330 100644 --- a/sickbeard/providers/shazbat.py +++ b/sickbeard/providers/shazbat.py @@ -40,12 +40,11 @@ class ShazbatProvider(generic.TorrentProvider): 'feeds': self.url_base + 'rss_feeds', 'browse': self.url_base + 'torrents?portlet=true', 'search': self.url_base + 'search?portlet=true&search=%s', - 'show': self.url_base + 'show?id=%s&show_mode=torrents', - 'get': self.url_base + '%s'} + 'show': self.url_base + 'show?id=%s&show_mode=torrents'} self.url = self.urls['config_provider_home_uri'] - self.username, self.password, self.minseed, self.minleech = 4 * [None] + self.username, self.password, self.scene, self.minseed, self.minleech = 5 * [None] def _authorised(self, **kwargs): @@ -138,11 +137,11 @@ class ShazbatProvider(generic.TorrentProvider): def _season_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._season_strings(self, ep_obj, detail_only=True, scene=False) + return super(ShazbatProvider, self)._season_strings(ep_obj, detail_only=True) def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, detail_only=True, scene=False, **kwargs) + return super(ShazbatProvider, self)._episode_strings(ep_obj, detail_only=True, **kwargs) provider = ShazbatProvider() diff --git a/sickbeard/providers/skytorrents.py b/sickbeard/providers/skytorrents.py new file mode 100644 index 00000000..984acf81 --- /dev/null +++ b/sickbeard/providers/skytorrents.py @@ -0,0 +1,107 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import re +import traceback + +from . import generic +from sickbeard import logger +from sickbeard.bs4_parser import BS4Parser +from sickbeard.helpers import tryInt +from lib.unidecode import unidecode + + +class SkytorrentsProvider(generic.TorrentProvider): + + def __init__(self): + + generic.TorrentProvider.__init__(self, 'Skytorrents', cache_update_freq=6) + + self.url_base = 'https://www.skytorrents.in/' + + self.urls = {'config_provider_home_uri': self.url_base, + 'search': self.url_base + 'search/all/ad/1/%s?l=en-us'} + + self.minseed, self.minleech = 2 * [None] + self.confirmed = False + + def _search_provider(self, search_params, **kwargs): + + results = [] + if not self._authorised(): + return results + + items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []} + + rc = dict((k, re.compile('(?i)' + v)) for (k, v) in { + 'info': '^/info/', 'get': '^(/file/|magnet:)', 'verified': 'Verified'}.items()) + + for mode in search_params.keys(): + for search_string in search_params[mode]: + search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string + search_url = self.urls['search'] % search_string + + html = self.get_url(search_url) + + cnt = len(items[mode]) + try: + if not html or self._has_no_results(html): + raise generic.HaltParseException + + with BS4Parser(html, features=['html5lib', 'permissive']) as soup: + torrent_table = soup.find('table', attrs={'class': ['table', 'is-striped']}) + torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') + + if 2 > len(torrent_rows): + raise generic.HaltParseException + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + try: + head = head if None is not head else self._header_row(tr) + seeders, leechers, size = [tryInt(n, n) for n in [ + cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] + if (self.confirmed and + not (tr.find('img', src=rc['verified']) or tr.find('img', title=rc['verified']))) \ + or self._peers_fail(mode, seeders, leechers): + continue + + info = tr.find('a', href=rc['info']) + title = (info.attrs.get('title') or info.get_text()).strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) + except (AttributeError, TypeError, ValueError, KeyError): + continue + + if title and download_url: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(items[mode]) - cnt, search_url) + + results = self._sort_seeding(mode, results + items[mode]) + + return results + + +provider = SkytorrentsProvider() diff --git a/sickbeard/providers/speedcd.py b/sickbeard/providers/speedcd.py index 103899cb..c6f4d501 100644 --- a/sickbeard/providers/speedcd.py +++ b/sickbeard/providers/speedcd.py @@ -26,25 +26,29 @@ from sickbeard.helpers import tryInt class SpeedCDProvider(generic.TorrentProvider): def __init__(self): - generic.TorrentProvider.__init__(self, 'SpeedCD', cache_update_freq=20) + generic.TorrentProvider.__init__(self, 'SpeedCD', cache_update_freq=20, update_freq=4*60) self.url_base = 'https://speed.cd/' self.urls = {'config_provider_home_uri': self.url_base, - 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'V3/API/API.php', - 'get': self.url_base + '%s'} + 'login': self.url_base + 'rss.php', + 'search': self.url_base + 'V3/API/API.php'} self.categories = {'Season': [41, 53], 'Episode': [2, 49, 50, 55], 'anime': [30]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] self.url = self.urls['config_provider_home_uri'] - self.username, self.password, self.freeleech, self.minseed, self.minleech = 5 * [None] + self.digest, self.freeleech, self.minseed, self.minleech = 4 * [None] def _authorised(self, **kwargs): return super(SpeedCDProvider, self)._authorised( - logged_in=(lambda y=None: self.has_all_cookies('inSpeed_speedian'))) + logged_in=(lambda y='': all( + [self.session.cookies.get_dict(domain='.speed.cd') and + self.session.cookies.clear('.speed.cd') is None or True] + + ['RSS' in y, 'type="password"' not in y, self.has_all_cookies(['speedian'], 'inSpeed_')] + + [(self.session.cookies.get('inSpeed_' + x) or 'sg!no!pw') in self.digest for x in ['speedian']])), + failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings')) def _search_provider(self, search_params, **kwargs): @@ -59,7 +63,6 @@ class SpeedCDProvider(generic.TorrentProvider): for mode in search_params.keys(): rc['cats'] = re.compile('(?i)cat=(?:%s)' % self._categories_string(mode, template='', delimiter='|')) for search_string in search_params[mode]: - search_string = '+'.join(search_string.split()) post_data = dict((x.split('=') for x in self._categories_string(mode).split('&')), search=search_string, jxt=2, jxw='b', freeleech=('on', None)[not self.freeleech]) @@ -67,7 +70,7 @@ class SpeedCDProvider(generic.TorrentProvider): cnt = len(items[mode]) try: - html = data_json.get('Fs')[0].get('Cn')[0].get('d') + html = data_json.get('Fs', [{}])[0].get('Cn', [{}])[0].get('d') if not html or self._has_no_results(html): raise generic.HaltParseException @@ -113,7 +116,12 @@ class SpeedCDProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='.', **kwargs) + return super(SpeedCDProvider, self)._episode_strings(ep_obj, scene=False, sep_date='.', **kwargs) + + @staticmethod + def ui_string(key): + + return 'speedcd_digest' == key and 'use... \'inSpeed_speedian=yy\'' or '' provider = SpeedCDProvider() diff --git a/sickbeard/providers/thepiratebay.py b/sickbeard/providers/thepiratebay.py index 179dd353..db861e91 100644 --- a/sickbeard/providers/thepiratebay.py +++ b/sickbeard/providers/thepiratebay.py @@ -17,6 +17,7 @@ from __future__ import with_statement +import base64 import os import re import traceback @@ -37,7 +38,51 @@ class ThePirateBayProvider(generic.TorrentProvider): generic.TorrentProvider.__init__(self, 'The Pirate Bay', cache_update_freq=20) self.url_home = ['https://thepiratebay.%s/' % u for u in 'se', 'org'] + \ - ['https://piratebay.usbypass.club/', 'https://tpb.run/'] + ['https://%s/' % base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('[t\sG]+', '', x[::-1]) for x in [ + 'mGGY', '5tGF', 'HGtc', 'vGGJ', 'Htte', 'uG k', '2GGd', 'uGtl']], + [re.sub('[t\sR]+', '', x[::-1]) for x in [ + 'uF2R a', 'it VWe', 'uk XRY', 'uR82RY', 'vt sWd', 'vR x2P', '9QWtRY']], + [re.sub('[n\sJ]+', '', x[::-1]) for x in [ + 'lGJnc', 'XJY y', 'YJlJR', '5 Fm', '5 niM', 'm cJv', '= Jc']], + [re.sub('[S\sp]+', '', x[::-1]) for x in [ + 'XYySSlGc', '5FmSYl R', 'CdzF SmZ', '15ypbSj5', 'Gb/8pSya', '=0DppZh9']], + [re.sub('[1\sz]+', '', x[::-1]) for x in [ + 'XYzy lGc', '5zFm1YlR', '2Yp1VzXc', 'u812 Yus', '2PvszW1d', '91zQWYvx']], + [re.sub('[P\sT]+', '', x[::-1]) for x in [ + 'lGPPc', 'XYP y', 'c l R', 'vTJTH', 'kT He', 'GdTPu', 'wPP9']], + [re.sub('[Y\sr]+', '', x[::-1]) for x in [ + 'J rHc', 'Hrrev', 'awYYl', 'hJYYX', 'U YGd', 'Gdr u', 'wr 9']], + [re.sub('[R\sk]+', '', x[::-1]) for x in [ + 'vJRkHc', '0 lHRe', 'uR IGc', 'iV2RRd', '0kl2Rc', '==kQ Z']], + [re.sub('[p\sz]+', '', x[::-1]) for x in [ + 'Hppc', '4pzJ', 'Sppe', 'wzz5', 'XppY', '0 zJ', 'Q pe', '=pz=']], + [re.sub('[p\si]+', '', x[::-1]) for x in [ + 'hGpid', 'Gai l', 'Z kpl', 'u ViG', 'FpmiY', 'mLii5', 'j N']], + [re.sub('[g\ss]+', '', x[::-1]) for x in [ + 'lhGgsd', 'ngFW b', '0s Vmb', '5sFmgY', 'uglsmL', '=8 m Z']], + [re.sub('[I\ss]+', '', x[::-1]) for x in [ + 'clIhsGd', 'X IYylG', 'Fm Yl R', '5IJmsL5', 'cszFGIc', 'nsLkIV2', '0I N']], + [re.sub('[ \sq]+', '', x[::-1]) for x in [ + 'GqclhG d', 'lR XqYyl', 'mL5Fm qY', 'uVXbt l', 'HdqpNqWa', '=Q3cuq k']], + [re.sub('[k\sK]+', '', x[::-1]) for x in [ + 'GKclh Gd', 'lRXKYyKl', 'nL5F mKY', 'vxmYkKuV', 'CZlKKt2Y', '=kw2bsk5']], + [re.sub('[f\si]+', '', x[::-1]) for x in [ + 'Gicl hGd', 'lRXiYfyl', 'nL5F imY', 'vximYfuV', 'CZlft 2Y', '==Adffz5']], + [re.sub('[j\sz]+', '', x[::-1]) for x in [ + 'G c lhGd', 'lRXYjy l', 'nL5FmjjY', 'v xmzYuV', 'Gbh t2 Y', 'nJ 3zbuw']], + [re.sub('[p\sH]+', '', x[::-1]) for x in [ + 'lHRXYylpGc', 'uVnL5FmY', 'yB3aj9HGpb', '1x2HYuo2b', 'spNmYwRnY', 'ulmLuFHWZ', '=8mZ']], + [re.sub('[1\sf]+', '', x[::-1]) for x in [ + 'H d', 'w1 B', 'm fc', '4 19', 'S e', 'z115', 'Xffa', 'l 1R']], + [re.sub('[r\sn]+', '', x[::-1]) for x in [ + 'Hr d', 'irnB', 'Hnrc', 'vn J', 'Hrne', 'u rk', '2rnd', 'unrl']], + [re.sub('[s\sZ]+', '', x[::-1]) for x in [ + 'H sd', 'iZ B', 'nssc', 'u V', 'nZZL', 'pZsd', 'g sb', '= s=']], + ]]] + ['http://%s' % base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('[q\sk]+', '', x[::-1]) for x in [ + 'mkYh5k2a', 'rR n LuV', '2avM3 L', 'vdGcqklV', 'nLnq5qWa', '19kDqcoB', '9kwm c']], + ]]] self.url_vars = {'search': 'search/%s/0/7/200', 'browse': 'tv/latest/'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'search': '%(home)s%(vars)s', @@ -95,7 +140,7 @@ class ThePirateBayProvider(generic.TorrentProvider): return None try: - my_parser = NameParser(showObj=self.show) + my_parser = NameParser(showObj=self.show, indexer_lookup=False) parse_result = my_parser.parse(file_name) except (InvalidNameException, InvalidShowException): return None @@ -124,10 +169,10 @@ class ThePirateBayProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, date_or=True, - ep_detail=lambda x: '%s|%s' % (config.naming_ep_type[2] % x, - config.naming_ep_type[0] % x), - ep_detail_anime=lambda x: '%02i' % x, **kwargs) + return super(ThePirateBayProvider, self)._episode_strings( + ep_obj, date_or=True, + ep_detail=lambda x: '%s|%s' % (config.naming_ep_type[2] % x, config.naming_ep_type[0] % x), + ep_detail_anime=lambda x: '%02i' % x, **kwargs) def _search_provider(self, search_params, search_mode='eponly', epcount=0, **kwargs): diff --git a/sickbeard/providers/tokyotoshokan.py b/sickbeard/providers/tokyotoshokan.py index b4807832..9dbbba70 100644 --- a/sickbeard/providers/tokyotoshokan.py +++ b/sickbeard/providers/tokyotoshokan.py @@ -99,7 +99,7 @@ class TokyoToshokanCache(tvcache.TVCache): self.update_freq = 15 - def _cache_data(self): + def _cache_data(self, **kwargs): mode = 'Cache' search_url = '%srss.php?%s' % (self.provider.url, urllib.urlencode({'filter': '1'})) diff --git a/sickbeard/providers/torlock.py b/sickbeard/providers/torlock.py index b5b561b8..9d8c2e75 100644 --- a/sickbeard/providers/torlock.py +++ b/sickbeard/providers/torlock.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import base64 import re import traceback import urllib @@ -31,7 +32,15 @@ class TorLockProvider(generic.TorrentProvider): def __init__(self): generic.TorrentProvider.__init__(self, 'TorLock') - self.url_home = ['https://www.torlock.com/'] + self.url_home = ['https://www.torlock.com/'] + \ + ['https://%s/' % base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('[X\sI]+', '', x[::-1]) for x in [ + 'yX9G d', 'j9 IGb', '1 I5ya', 'sJmX b', 'rN2I b', 'uQXXWZ', '0IVmIY']], + [re.sub('[w\sP]+', '', x[::-1]) for x in [ + 'y9PPGd', 'jPw9Gb', '1wP5ya', 's JmPb', 'rPN2wb', 'uQPPWZ', 'klm wY']], + [re.sub('[g\sv]+', '', x[::-1]) for x in [ + 'yB Dgd', 'jgg9Gb', '1vv5ya', 'svvJmb', 'rN2vgb', 'uQ vWZ', 'sg9 Gb']], + ]]] self.url_vars = {'search': 'television/torrents/%s.html?sort=added&order=desc', 'browse': 'television/1/added/desc.html', 'get': 'tor/%s.torrent'} diff --git a/sickbeard/providers/torrentbytes.py b/sickbeard/providers/torrentbytes.py index bb072acb..44da04a0 100644 --- a/sickbeard/providers/torrentbytes.py +++ b/sickbeard/providers/torrentbytes.py @@ -30,11 +30,11 @@ class TorrentBytesProvider(generic.TorrentProvider): def __init__(self): generic.TorrentProvider.__init__(self, 'TorrentBytes', cache_update_freq=20) - self.url_home = ['https://www.torrentbytes.net/'] + self.url_home = ['https://www.torrentbytes.%s/' % u for u in 'net', 'me'] - self.url_vars = {'login_action': 'login.php', 'search': 'browse.php?search=%s&%s', 'get': '%s'} + self.url_vars = {'login_action': 'login.php', 'search': 'browse.php?search=%s&%s'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login_action': '%(home)s%(vars)s', - 'search': '%(home)s%(vars)s', 'get': '%(home)s%(vars)s'} + 'search': '%(home)s%(vars)s'} self.categories = {'Season': [41, 32], 'Episode': [33, 37, 38]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] diff --git a/sickbeard/providers/torrentday.py b/sickbeard/providers/torrentday.py index 65c975d8..7cda7d27 100644 --- a/sickbeard/providers/torrentday.py +++ b/sickbeard/providers/torrentday.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import base64 import re import time @@ -29,11 +30,23 @@ class TorrentDayProvider(generic.TorrentProvider): generic.TorrentProvider.__init__(self, 'TorrentDay') self.url_home = ['https://%s/' % u for u in 'torrentday.eu', 'secure.torrentday.com', 'tdonline.org', - 'torrentday.it', 'www.td.af', 'www.torrentday.com'] + 'torrentday.it', 'www.td.af', 'www.torrentday.com'] + \ + ['http://td.%s/' % base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('(?i)[I\s1]+', '', x[::-1]) for x in [ + 'y92d', 'zl12a', 'y9mY', 'n5 Wa', 'vNmIL', '=i1=Qb']], + [re.sub('(?i)[T\sq]+', '', x[::-1]) for x in [ + '15TWd', 'hV 3c', 'lBHb', 'vNncq', 'j5ib', '=qQ02b']], + [re.sub('(?i)[0\so]+', '', x[::-1]) for x in [ + 'Vmco', 'CZh', 'boi10', 'r92', '5yc', 'mcv', '=oc']], + [re.sub('(?i)[1\slq]+', '', x[::-1]) for x in [ + '2cql', 'yV1', 'mdlq', 'wQV', 'n11M', 'uA', '12Y', 't9']], + [re.sub('(?i)[\s1q]+', '', x[::-1]) for x in [ + 'Vmbq', 'WL10', 'ZyZ', 'rFW', '5yc', '12bj', 'q=0']] + ]]] - self.url_vars = {'login': 'rss.php', 'search': 't?%s%s;q=%s%s', 'get': '%s'} + self.url_vars = {'login': 'rss.php', 'search': 'browse.php?cata=yes&%s%s&search=%s%s'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login': '%(home)s%(vars)s', - 'search': '%(home)s%(vars)s', 'get': '%(home)s%(vars)s'} + 'search': '%(home)s%(vars)s'} self.categories = {'Season': [31, 33, 14], 'Episode': [24, 32, 26, 7, 34, 2], 'anime': [29]} self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] @@ -69,8 +82,8 @@ class TorrentDayProvider(generic.TorrentProvider): search_string = '+'.join(search_string.split()) search_url = self.urls['search'] % ( - self._categories_string(mode, '%s', ';'), (';free', '')[not self.freeleech], - search_string, (';o=seeders', '')['Cache' == mode]) + self._categories_string(mode), ('&free=on', '')[not self.freeleech], + search_string, ('&sort=7&type=desc', '')['Cache' == mode]) html = self.get_url(search_url) @@ -79,7 +92,7 @@ class TorrentDayProvider(generic.TorrentProvider): if not html or self._has_no_results(html): raise generic.HaltParseException - with BS4Parser(html, features=['html5lib', 'permissive']) as soup: + with BS4Parser(html, features=['html5lib', 'permissive'], tag='table', attr='torrentTable') as soup: torrent_table = soup.find('table', id='torrentTable') torrent_rows = [] if not torrent_table else torrent_table.find_all('tr') @@ -120,7 +133,7 @@ class TorrentDayProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='.', date_or=True, **kwargs) + return super(TorrentDayProvider, self)._episode_strings(ep_obj, sep_date='.', date_or=True, **kwargs) def ui_string(self, key): if 'torrentday_digest' == key and self._valid_home(): diff --git a/sickbeard/providers/torrenting.py b/sickbeard/providers/torrenting.py index eb926648..c6d83130 100644 --- a/sickbeard/providers/torrenting.py +++ b/sickbeard/providers/torrenting.py @@ -32,9 +32,9 @@ class TorrentingProvider(generic.TorrentProvider): self.url_home = ['https://%s/' % u for u in 'www.torrenting.com', 'ttonline.us'] - self.url_vars = {'login': 'rss.php', 'search': 'browse.php?%s&search=%s', 'get': '%s'} + self.url_vars = {'login': 'rss.php', 'search': 'browse.php?%s&search=%s'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login': '%(home)s%(vars)s', - 'search': '%(home)s%(vars)s', 'get': '%(home)s%(vars)s'} + 'search': '%(home)s%(vars)s'} self.categories = {'shows': [4, 5]} diff --git a/sickbeard/providers/torrentleech.py b/sickbeard/providers/torrentleech.py index 7da261a1..fb4a31ba 100644 --- a/sickbeard/providers/torrentleech.py +++ b/sickbeard/providers/torrentleech.py @@ -33,8 +33,7 @@ class TorrentLeechProvider(generic.TorrentProvider): self.urls = {'config_provider_home_uri': self.url_base, 'login_action': self.url_base, 'browse': self.url_base + 'torrents/browse/index/categories/%(cats)s', - 'search': self.url_base + 'torrents/browse/index/query/%(query)s/categories/%(cats)s', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'torrents/browse/index/query/%(query)s/categories/%(cats)s'} self.categories = {'shows': [2, 26, 27, 32], 'anime': [7, 34, 35]} @@ -110,7 +109,7 @@ class TorrentLeechProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='|', **kwargs) + return super(TorrentLeechProvider, self)._episode_strings(ep_obj, sep_date='|', **kwargs) provider = TorrentLeechProvider() diff --git a/sickbeard/providers/torrentshack.py b/sickbeard/providers/torrentvault.py similarity index 64% rename from sickbeard/providers/torrentshack.py rename to sickbeard/providers/torrentvault.py index 14988199..8f396266 100644 --- a/sickbeard/providers/torrentshack.py +++ b/sickbeard/providers/torrentvault.py @@ -1,7 +1,5 @@ # coding=utf-8 # -# Author: SickGear -# # This file is part of SickGear. # # SickGear is free software: you can redistribute it and/or modify @@ -27,29 +25,32 @@ from sickbeard.helpers import tryInt from lib.unidecode import unidecode -class TorrentShackProvider(generic.TorrentProvider): +class TorrentVaultProvider(generic.TorrentProvider): def __init__(self): - generic.TorrentProvider.__init__(self, 'TorrentShack', cache_update_freq=20) + generic.TorrentProvider.__init__(self, 'TorrentVault', cache_update_freq=10) - self.url_base = 'https://torrentshack.me/' - self.urls = {'config_provider_home_uri': self.url_base, - 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'torrents.php?searchstr=%s&%s&' + '&'.join( - ['release_type=both', 'searchtags=', 'tags_type=0', - 'order_by=s3', 'order_way=desc', 'torrent_preset=all']), - 'get': self.url_base + '%s'} + self.url_home = ['https://www.torrentvault.org/'] - self.categories = {'shows': [600, 620, 700, 981, 980], 'anime': [850]} + self.url_vars = {'login_action': 'login.php', 'search': 'torrents.php?%s' % '&'.join( + ['searchstr=%s', 'order_by=s3', 'order_way=desc', 'action=basic', '%s'])} + self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login_action': '%(home)s%(vars)s', + 'search': '%(home)s%(vars)s'} - self.url = self.urls['config_provider_home_uri'] + self.categories = {'Season': [7, 32], 'Episode': [4, 8, 9]} + self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] - self.username, self.password, self.minseed, self.minleech = 4 * [None] + self.username, self.password, self.freeleech, self.minseed, self.minleech = 5 * [None] def _authorised(self, **kwargs): - return super(TorrentShackProvider, self)._authorised(logged_in=(lambda y=None: self.has_all_cookies('session')), - post_params={'keeplogged': '1', 'form_tmpl': True}) + return super(TorrentVaultProvider, self)._authorised( + logged_in=(lambda y=None: self.has_all_cookies('keeplogged')), post_params={'form_tmpl': True}) + + @staticmethod + def _has_signature(data=None): + + return generic.TorrentProvider._has_signature(data) or (data and re.search(r'(?i) len(torrent_rows): @@ -90,13 +90,13 @@ class TorrentShackProvider(generic.TorrentProvider): head = head if None is not head else self._header_row(tr) seeders, leechers, size = [tryInt(n, n) for n in [ cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] - if self._peers_fail(mode, seeders, leechers): + if (self.freeleech and not rc['filter'].search(cells[1].get_text())) \ + or self._peers_fail(mode, seeders, leechers): continue - size = rc['size'].sub('', size) - info = tr.find('a', title=rc['info']) - title = (rc['title'].sub('', info.attrs.get('title', '')) or info.get_text()).strip() - download_url = self._link(tr.find('a', title=rc['get'])['href']) + info = tr.find('a', href=rc['info']) + title = (info.attrs.get('title') or info.get_text()).strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) except (AttributeError, TypeError, ValueError, KeyError): continue @@ -107,6 +107,7 @@ class TorrentShackProvider(generic.TorrentProvider): pass except (StandardError, Exception): logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + self._log_search(mode, len(items[mode]) - cnt, search_url) results = self._sort_seeding(mode, results + items[mode]) @@ -115,7 +116,7 @@ class TorrentShackProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='.', **kwargs) + return super(TorrentVaultProvider, self)._episode_strings(ep_obj, sep_date='.', **kwargs) -provider = TorrentShackProvider() +provider = TorrentVaultProvider() diff --git a/sickbeard/providers/torrentz2.py b/sickbeard/providers/torrentz2.py index 5b140d29..a4902c57 100644 --- a/sickbeard/providers/torrentz2.py +++ b/sickbeard/providers/torrentz2.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import base64 import re import time import traceback @@ -32,7 +33,35 @@ class Torrentz2Provider(generic.TorrentProvider): def __init__(self): generic.TorrentProvider.__init__(self, 'Torrentz2') - self.url_home = ['https://torrentz2.eu/'] + self.url_home = ['https://torrentz2.eu/'] + \ + ['https://%s/' % base64.b64decode(x) for x in [''.join(x) for x in [ + [re.sub('[ \sQ]+', '', x[::-1]) for x in [ + 'GQQd', 'yQQ9', 'mQ c', 'uQ V', 'HQd', 'yQ o', 'm L', 'z Ql']], + [re.sub('[S\sl]+', '', x[::-1]) for x in [ + 'Glld', 'yll9', 'ml c', 'uSlV', 'HS d', 'yl o', 'mSSL', 'jS N']], + [re.sub('[1\sq]+', '', x[::-1]) for x in [ + 'G qd', 'y 9', 'm1qc', 'u1 V', 'H 1d', 'yqqo', 'n1qL', '21 R']], + [re.sub('[F\sf]+', '', x[::-1]) for x in [ + 'G fd', 'y 9', 'm c', 'u FV', 'H Fd', 'uf o', 'nffY', '=fFo']], + [re.sub('[j\sF]+', '', x[::-1]) for x in [ + 'cy9F Gd', 'HFdFuVm', 'lnFYu o', 'zNXFY w', 'Yu jQWZ', 'AbFv9F2', '=FF=']], + [re.sub('[K\sP]+', '', x[::-1]) for x in [ + 'yK9 Gd', 'uVm Pc', 'uoH Pd', 'w lnY', 'zP NXY', 'uQ PWZ', '=QK3Pc']], + [re.sub('[R\sh]+', '', x[::-1]) for x in [ + 'cyhR9Gd', 'HRdhuVm', '1hWaRuo', 'p5RWdRt', 'e0l2h Y', 'Adz h5S', '= R=']], + [re.sub('[K\s ]+', '', x[::-1]) for x in [ + 'cKy9G d', 'Hdu KVm', 'tWdu o', 'sJKKmb1', 'Lr N 2b', 'g bKl1m', '= K=']], + [re.sub('[s\sx]+', '', x[::-1]) for x in [ + 'cy 9G d', 'HdxxuVm', '5xWsduo', 'j9Gb si', 'L skV2a', 'AZp Jsm', '=s =']], + [re.sub('[P\s ]+', '', x[::-1]) for x in [ + 'cy9 PGd', 'H duPVm', '5WdPu o', 'jP 9Gbi', 'LPkV 2a', 'APbvx m', '=PP=']], + [re.sub('[X\sP]+', '', x[::-1]) for x in [ + 'yP9XGd', 'uVm Pc', 'uX oHd', 'i X5Wd', 'j 9GXb', 'k VX2a', '0PXNnL']], + [re.sub('[w\sf]+', '', x[::-1]) for x in [ + 'cwy9Gfd', 'H duV m', '5Wfwduo', 'j9G bfi', 'bsFw2 a', 'mwcfv5C', '=ffc']], + [re.sub('[Z\sj]+', '', x[::-1]) for x in [ + 'm cjy9Gd', 'uZoHduZV', '2bs5jWZd', 'vJZHcZrN', 'XYw 5 ia', '==Qejj0J']], + ]]] self.url_vars = {'search': 'searchA?f=%s&safe=1', 'searchv': 'verifiedA?f=%s&safe=1'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', @@ -109,8 +138,8 @@ class Torrentz2Provider(generic.TorrentProvider): return results def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings( - self, ep_obj, date_detail=(lambda d: [x % str(d).replace('-', '.') for x in ('"%s"', '%s')]), + return super(Torrentz2Provider, self)._episode_strings( + ep_obj, date_detail=(lambda d: [x % str(d).replace('-', '.') for x in ('"%s"', '%s')]), ep_detail=(lambda ep_dict: [x % (config.naming_ep_type[2] % ep_dict) for x in ('"%s"', '%s')]), **kwargs) diff --git a/sickbeard/providers/tvchaosuk.py b/sickbeard/providers/tvchaosuk.py index de51e85c..146ad31a 100644 --- a/sickbeard/providers/tvchaosuk.py +++ b/sickbeard/providers/tvchaosuk.py @@ -21,8 +21,8 @@ import traceback from . import generic from sickbeard import logger from sickbeard.bs4_parser import BS4Parser -from sickbeard.helpers import tryInt from sickbeard.config import naming_ep_type +from sickbeard.helpers import tryInt from dateutil.parser import parse from lib.unidecode import unidecode @@ -35,8 +35,7 @@ class TVChaosUKProvider(generic.TorrentProvider): self.url_base = 'https://www.tvchaosuk.com/' self.urls = {'config_provider_home_uri': self.url_base, 'login_action': self.url_base + 'login.php', - 'search': self.url_base + 'browse.php', - 'get': self.url_base + '%s'} + 'search': self.url_base + 'browse.php'} self.url = self.urls['config_provider_home_uri'] @@ -59,6 +58,7 @@ class TVChaosUKProvider(generic.TorrentProvider): rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'info': 'detail', 'get': 'download', 'fl': 'free'}.items()) for mode in search_params.keys(): for search_string in search_params[mode]: + search_string = search_string.replace(u'£', '%') search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string kwargs = dict(post_data={'keywords': search_string, 'do': 'quick_sort', 'page': '0', @@ -115,9 +115,10 @@ class TVChaosUKProvider(generic.TorrentProvider): get_detail = False try: - title = self.regulate_title(title, mode) - if title and download_url: - items[mode].append((title, download_url, seeders, self._bytesizer(size))) + titles = self.regulate_title(title, mode, search_string) + if download_url and titles: + for title in titles: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) except (StandardError, Exception): pass @@ -127,7 +128,7 @@ class TVChaosUKProvider(generic.TorrentProvider): logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) self._log_search(mode, len(items[mode]) - cnt, - ('search string: ' + search_string.replace('%', ' '), self.name)['Cache' == mode]) + ('search string: ' + search_string.replace('%', '%%'), self.name)['Cache' == mode]) if mode in 'Season' and len(items[mode]): break @@ -137,7 +138,7 @@ class TVChaosUKProvider(generic.TorrentProvider): return results @staticmethod - def regulate_title(title, mode='-'): + def regulate_title(title, mode='-', search_string=''): has_series = re.findall('(?i)(.*?series[^\d]*?\d+)(.*)', title) if has_series: @@ -163,13 +164,14 @@ class TVChaosUKProvider(generic.TorrentProvider): for yr in years: title = re.sub('\{\{yr\}\}', yr, title, count=1) - dated = re.findall('(?i)([(\s]*)((?:\d+\s)?)([adfjmnos]\w{2,}\s+)((?:19|20)\d\d)([)\s]*)', title) + date_re = '(?i)([(\s.]*)((?:\d+[\s.]*(?:st|nd|rd|th)?[\s.])?)([adfjmnos]\w{2,}[\s.]+)((?:19|20)\d\d)([)\s.]*)' + dated = re.findall(date_re, title) + dnew = None for d in dated: try: dout = parse(''.join(d[1:4])).strftime('%Y-%m-%d') - title = title.replace(''.join(d), '%s%s%s' % ( - ('', ' ')[1 < len(d[0])], dout[0: not any(d[2]) and 4 or not any(d[1]) and 7 or len(dout)], - ('', ' ')[1 < len(d[4])])) + dnew = dout[0: not any(d[2]) and 4 or not any(d[1]) and 7 or len(dout)] + title = title.replace(''.join(d), '%s%s%s' % (('', ' ')[1 < len(d[0])], dnew, ('', ' ')[1 < len(d[4])])) except (StandardError, Exception): pass if dated: @@ -212,7 +214,30 @@ class TVChaosUKProvider(generic.TorrentProvider): for r in [('\s+[-]?\s+|\s+`|`\s+', '`'), ('`+', '.')]: title = re.sub(r[0], r[1], title) - return title + titles = [] + if dnew: + snew = None + dated_s = re.findall(date_re, search_string) + for d in dated_s: + try: + sout = parse(''.join(d[1:4])).strftime('%Y-%m-%d') + snew = sout[0: not any(d[2]) and 4 or not any(d[1]) and 7 or len(sout)] + except (StandardError, Exception): + pass + + if snew and dnew and snew != dnew: + return titles + + try: + sxxexx_r = '(?i)S\d\d+E\d\d+' + if dnew and re.search(sxxexx_r, title): + titles += [re.sub(sxxexx_r, dnew, re.sub('[_.\-\s]?%s' % dnew, '', title))] + except (StandardError, Exception): + pass + + titles += [title] + + return titles def _season_strings(self, ep_obj, **kwargs): @@ -223,8 +248,10 @@ class TVChaosUKProvider(generic.TorrentProvider): def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, scene=False, prefix='%', date_detail=( - lambda d: [d.strftime('%d %b %Y')] + ([d.strftime('%d %B %Y')], [])[d.strftime('%b') == d.strftime('%B')]), + return super(TVChaosUKProvider, self)._episode_strings(ep_obj, scene=False, prefix='%', date_detail=( + lambda d: [x.strip('0') for x in ( + ['{0} {1}% {2}'.format(d.strftime('%d')[-1], d.strftime('%b'), d.strftime('%Y'))] + + [d.strftime('%d %b %Y')] + ([d.strftime('%d %B %Y')], [])[d.strftime('%b') == d.strftime('%B')])]), ep_detail=(lambda e: [naming_ep_type[2] % e] + ( [], ['%(episodenumber)dof' % e])[1 == tryInt(e.get('seasonnumber'))]), **kwargs) diff --git a/sickbeard/providers/womble.py b/sickbeard/providers/womble.py deleted file mode 100644 index 80b3b5bb..00000000 --- a/sickbeard/providers/womble.py +++ /dev/null @@ -1,57 +0,0 @@ -# Author: Nic Wolfe -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of SickGear. -# -# SickGear is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SickGear is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SickGear. If not, see . - -from . import generic -from sickbeard import tvcache -import time - - -class WombleProvider(generic.NZBProvider): - - def __init__(self): - generic.NZBProvider.__init__(self, 'Womble\'s Index', supports_backlog=False) - - self.url = 'https://newshost.co.za/' - self.cache = WombleCache(self) - - -class WombleCache(tvcache.TVCache): - - def __init__(self, this_provider): - tvcache.TVCache.__init__(self, this_provider) - - self.update_freq = 6 - - def _cache_data(self): - - result = [] - for section in ['sd', 'hd', 'x264', 'dvd']: - url = '%srss/?sec=tv-%s&fr=false' % (self.provider.url, section) - xml_data = self.getRSSFeed(url) - time.sleep(1.1) - cnt = len(result) - for entry in (xml_data and xml_data.get('entries', []) or []): - if entry.get('title') and entry.get('link', '').startswith('http'): - result.append((entry.get('title'), entry.get('link'), None, None)) - - self.provider.log_result(count=len(result) - cnt, url=url) - - return result - - -provider = WombleProvider() diff --git a/sickbeard/providers/wop.py b/sickbeard/providers/wop.py new file mode 100644 index 00000000..0647cd92 --- /dev/null +++ b/sickbeard/providers/wop.py @@ -0,0 +1,138 @@ +# coding=utf-8 +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import re +import traceback + +from . import generic +from sickbeard import logger +from sickbeard.bs4_parser import BS4Parser +from sickbeard.helpers import tryInt +from lib.unidecode import unidecode + + +class WOPProvider(generic.TorrentProvider): + + def __init__(self): + generic.TorrentProvider.__init__(self, 'WOP', cache_update_freq=10) + + self.url_home = ['https://www.worldofp2p.net/'] + + self.url_vars = {'login': 'getrss.php', 'search': 'browse.php?%s' % '&'.join( + ['search=%s', 'searchin=title', 'incldead=0', 'sort=4', 'type=desc', '%s'])} + self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'login': '%(home)s%(vars)s', + 'search': '%(home)s%(vars)s'} + + self.categories = {'Season': [41], 'Episode': [35, 5, 58, 42, 36, 55, 39, 37, 54, 38]} + self.categories['Cache'] = self.categories['Season'] + self.categories['Episode'] + + self.digest, self.freeleech, self.minseed, self.minleech = 4 * [None] + + def _authorised(self, **kwargs): + + return super(WOPProvider, self)._authorised( + logged_in=(lambda y=None: all( + [(None is y or re.search('(?i)rss\slink', y)), self.has_all_cookies()] + + [(self.session.cookies.get(x) or 'sg!no!pw') in self.digest for x in ['hashv']])), + failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings')) + + @staticmethod + def _has_signature(data=None): + return generic.TorrentProvider._has_signature(data) or (data and re.search(r'(?sim) len(torrent_rows): + raise generic.HaltParseException + + head = None + for tr in torrent_rows[1:]: + cells = tr.find_all('td') + if 5 > len(cells): + continue + try: + head = head if None is not head else self._header_row( + tr, custom_tags=[('span', 'data-original-title')]) + seeders, leechers, size = [n for n in [ + cells[head[x]].get_text().strip() for x in 'seed', 'leech', 'size']] + if (self.freeleech and not tr.find('i', class_=rc['filter'])) \ + or self._peers_fail(mode, seeders, leechers): + continue + + title = tr.find('a', href=rc['info']).get_text().strip() + download_url = self._link(tr.find('a', href=rc['get'])['href']) + except (AttributeError, TypeError, ValueError, KeyError): + continue + + if title and download_url: + items[mode].append((title, download_url, seeders, self._bytesizer(size))) + + except generic.HaltParseException: + pass + except (StandardError, Exception): + logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR) + + self._log_search(mode, len(items[mode]) - cnt, search_url) + + results = self._sort_seeding(mode, results + items[mode]) + + return results + + def _season_strings(self, ep_obj, **kwargs): + + return self._search_params(super(WOPProvider, self)._season_strings(ep_obj, **kwargs)) + + def _episode_strings(self, ep_obj, **kwargs): + + return self._search_params(super(WOPProvider, self)._episode_strings(ep_obj, **kwargs)) + + @staticmethod + def _search_params(search_params): + + return [dict((k, ['*%s*' % re.sub('[.\s]', '*', v) for v in v]) for k, v in d.items()) for d in search_params] + + @staticmethod + def ui_string(key): + + return 'wop_digest' == key and 'use... \'uid=xx; pass=yy; hashv=zz\'' or '' + + +provider = WOPProvider() diff --git a/sickbeard/providers/zooqle.py b/sickbeard/providers/zooqle.py index b35a7b11..bfd81253 100644 --- a/sickbeard/providers/zooqle.py +++ b/sickbeard/providers/zooqle.py @@ -32,7 +32,7 @@ class ZooqleProvider(generic.TorrentProvider): self.url_base = 'https://zooqle.com/' self.urls = {'config_provider_home_uri': self.url_base, - 'search': self.url_base + 'search?q=%s category:%s&s=dt&v=t&sd=d', + 'search': self.url_base + 'search?q=%s category:%s&s=ns&v=t&sd=d', 'get': self.url_base + 'download/%s.torrent'} self.categories = {'Season': ['TV'], 'Episode': ['TV'], 'anime': ['Anime']} @@ -107,9 +107,10 @@ class ZooqleProvider(generic.TorrentProvider): return results def _episode_strings(self, ep_obj, **kwargs): - return generic.TorrentProvider._episode_strings(self, ep_obj, sep_date='.', **kwargs) + return super(ZooqleProvider, self)._episode_strings(ep_obj, sep_date='.', **kwargs) - def _cache_data(self): + def _cache_data(self, **kwargs): return self._search_provider({'Cache': ['*']}) + provider = ZooqleProvider() diff --git a/sickbeard/rssfeeds.py b/sickbeard/rssfeeds.py index a10ffee9..04226f12 100644 --- a/sickbeard/rssfeeds.py +++ b/sickbeard/rssfeeds.py @@ -3,7 +3,7 @@ # This file is part of SickGear. # -from feedparser import feedparser +import feedparser from sickbeard import helpers, logger from sickbeard.exceptions import ex diff --git a/sickbeard/sab.py b/sickbeard/sab.py index 0120d6a4..678d7af6 100644 --- a/sickbeard/sab.py +++ b/sickbeard/sab.py @@ -60,7 +60,10 @@ def send_nzb(nzb): nzb_type = 'file nzb' params['mode'] = 'addfile' kwargs['post_data'] = params - kwargs['files'] = {'nzbfile': ('%s.nzb' % nzb.name, nzb.extraInfo[0])} + nzb_data = nzb.get_data() + if not nzb_data: + return False + kwargs['files'] = {'nzbfile': ('%s.nzb' % nzb.name, nzb_data)} logger.log(u'Sending %s to SABnzbd: %s' % (nzb_type, nzb.name)) diff --git a/sickbeard/scene_exceptions.py b/sickbeard/scene_exceptions.py index b3bb2cf4..2670f9cf 100644 --- a/sickbeard/scene_exceptions.py +++ b/sickbeard/scene_exceptions.py @@ -21,6 +21,7 @@ import time import threading import datetime import sickbeard +import traceback from collections import defaultdict from lib import adba @@ -210,7 +211,12 @@ def retrieve_exceptions(): continue for cur_exception_dict in exception_dict[cur_indexer_id]: - cur_exception, cur_season = cur_exception_dict.items()[0] + try: + cur_exception, cur_season = cur_exception_dict.items()[0] + except Exception: + logger.log('scene exception error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) + continue # if this exception isn't already in the DB then add it if cur_exception not in existing_exceptions: @@ -289,7 +295,7 @@ def _xem_exceptions_fetcher(): break if shouldRefresh(xem_list): - for indexer in sickbeard.indexerApi().indexers: + for indexer in [i for i in sickbeard.indexerApi().indexers if 'xem_origin' in sickbeard.indexerApi(i).config]: logger.log(u'Checking for XEM scene exception updates for %s' % sickbeard.indexerApi(indexer).name) url = 'http://thexem.de/map/allNames?origin=%s%s&seasonNumbers=1'\ diff --git a/sickbeard/scene_numbering.py b/sickbeard/scene_numbering.py index c4719abd..f4a8988b 100644 --- a/sickbeard/scene_numbering.py +++ b/sickbeard/scene_numbering.py @@ -461,7 +461,7 @@ def xem_refresh(indexer_id, indexer, force=False): indexer_id = int(indexer_id) indexer = int(indexer) - if indexer_id not in xem_ids_list[indexer]: + if 'xem_origin' not in sickbeard.indexerApi(indexer).config or indexer_id not in xem_ids_list.get(indexer, []): return # XEM API URL @@ -529,7 +529,7 @@ def xem_refresh(indexer_id, indexer, force=False): logger.log( u'Exception while refreshing XEM data for show ' + str(indexer_id) + ' on ' + sickbeard.indexerApi( indexer).name + ': ' + ex(e), logger.WARNING) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) def fix_xem_numbering(indexer_id, indexer): diff --git a/sickbeard/scheduler.py b/sickbeard/scheduler.py index e2713ff3..562188fa 100644 --- a/sickbeard/scheduler.py +++ b/sickbeard/scheduler.py @@ -39,6 +39,7 @@ class Scheduler(threading.Thread): self.name = threadName self.silent = silent self.stop = threading.Event() + self.lock = threading.Lock() self.force = False def timeLeft(self): @@ -90,7 +91,7 @@ class Scheduler(threading.Thread): self.action.run() except Exception as e: logger.log(u"Exception generated in thread " + self.name + ": " + ex(e), logger.ERROR) - logger.log(repr(traceback.format_exc()), logger.DEBUG) + logger.log(repr(traceback.format_exc()), logger.ERROR) finally: if self.force: diff --git a/sickbeard/search.py b/sickbeard/search.py index 1dbbd566..4aca07ff 100644 --- a/sickbeard/search.py +++ b/sickbeard/search.py @@ -72,10 +72,14 @@ def _download_result(result): # save the data to disk try: - with ek.ek(open, file_name, 'w') as file_out: - file_out.write(result.extraInfo[0]) + data = result.get_data() + if not data: + new_result = False + else: + with ek.ek(open, file_name, 'w') as file_out: + file_out.write(data) - helpers.chmodAsParent(file_name) + helpers.chmodAsParent(file_name) except EnvironmentError as e: logger.log(u'Error trying to save NZB to black hole: %s' % ex(e), logger.ERROR) @@ -109,7 +113,7 @@ def snatch_episode(result, end_status=SNATCHED): for cur_ep in result.episodes: if datetime.date.today() - cur_ep.airdate <= datetime.timedelta(days=7): result.priority = 1 - if None is not re.search('(^|[. _-])(proper|repack)([. _-]|$)', result.name, re.I): + if 0 < result.properlevel: end_status = SNATCHED_PROPER # NZBs can be sent straight to SAB or saved to disk @@ -119,8 +123,7 @@ def snatch_episode(result, end_status=SNATCHED): elif 'sabnzbd' == sickbeard.NZB_METHOD: dl_result = sab.send_nzb(result) elif 'nzbget' == sickbeard.NZB_METHOD: - is_proper = True if SNATCHED_PROPER == end_status else False - dl_result = nzbget.send_nzb(result, is_proper) + dl_result = nzbget.send_nzb(result) else: logger.log(u'Unknown NZB action specified in config: %s' % sickbeard.NZB_METHOD, logger.ERROR) dl_result = False @@ -140,6 +143,9 @@ def snatch_episode(result, end_status=SNATCHED): # Snatches torrent with client client = clients.get_client_instance(sickbeard.TORRENT_METHOD)() dl_result = client.send_torrent(result) + + if getattr(result, 'cache_file', None): + helpers.remove_file_failed(result.cache_file) else: logger.log(u'Unknown result type, unable to download it', logger.ERROR) dl_result = False @@ -148,11 +154,11 @@ def snatch_episode(result, end_status=SNATCHED): return False if sickbeard.USE_FAILED_DOWNLOADS: - failed_history.logSnatch(result) + failed_history.add_snatched(result) ui.notifications.message(u'Episode snatched', result.name) - history.logSnatch(result) + history.log_snatch(result) # don't notify when we re-download an episode sql_l = [] @@ -201,39 +207,40 @@ def pick_best_result(results, show, quality_list=None): best_result = None for cur_result in results: - logger.log(u'Quality is %s for %s' % (Quality.qualityStrings[cur_result.quality], cur_result.name)) + logger.log(u'Quality is %s for [%s]' % (Quality.qualityStrings[cur_result.quality], cur_result.name)) if show.is_anime and not show.release_groups.is_valid(cur_result): continue if quality_list and cur_result.quality not in quality_list: - logger.log(u'%s is an unwanted quality, rejecting it' % cur_result.name, logger.DEBUG) + logger.log(u'Rejecting unwanted quality [%s]' % cur_result.name, logger.DEBUG) continue if not pass_show_wordlist_checks(cur_result.name, show): continue cur_size = getattr(cur_result, 'size', None) - if sickbeard.USE_FAILED_DOWNLOADS and None is not cur_size and failed_history.hasFailed( + if sickbeard.USE_FAILED_DOWNLOADS and None is not cur_size and failed_history.has_failed( cur_result.name, cur_size, cur_result.provider.name): - logger.log(u'%s has previously failed, rejecting it' % cur_result.name) + logger.log(u'Rejecting previously failed [%s]' % cur_result.name) continue if not best_result or best_result.quality < cur_result.quality != Quality.UNKNOWN: best_result = cur_result elif best_result.quality == cur_result.quality: - if re.search('(?i)(proper|repack)', cur_result.name) or \ - show.is_anime and re.search('(?i)(v1|v2|v3|v4|v5)', cur_result.name): - best_result = cur_result - elif 'internal' in best_result.name.lower() and 'internal' not in cur_result.name.lower(): - best_result = cur_result - elif 'xvid' in best_result.name.lower() and 'x264' in cur_result.name.lower(): - logger.log(u'Preferring %s (x264 over xvid)' % cur_result.name) + if cur_result.properlevel > best_result.properlevel and \ + (not cur_result.is_repack or cur_result.release_group == best_result.release_group): best_result = cur_result + elif cur_result.properlevel == best_result.properlevel: + if 'xvid' in best_result.name.lower() and 'x264' in cur_result.name.lower(): + logger.log(u'Preferring (x264 over xvid) [%s]' % cur_result.name) + best_result = cur_result + elif 'internal' in best_result.name.lower() and 'internal' not in cur_result.name.lower(): + best_result = cur_result if best_result: - logger.log(u'Picked %s as the best' % best_result.name, logger.DEBUG) + logger.log(u'Picked as the best [%s]' % best_result.name, logger.DEBUG) else: logger.log(u'No result picked.', logger.DEBUG) @@ -345,7 +352,7 @@ def wanted_episodes(show, from_date, make_dict=False, unaired=False): else: wanted = [] total_wanted = total_replacing = total_unaired = 0 - downloaded_status_list = (common.DOWNLOADED, common.SNATCHED, common.SNATCHED_PROPER, common.SNATCHED_BEST) + downloaded_status_list = common.SNATCHED_ANY + [common.DOWNLOADED] for result in sql_results: not_downloaded = True cur_composite_status = int(result['status']) @@ -388,6 +395,11 @@ def wanted_episodes(show, from_date, make_dict=False, unaired=False): ep_obj = show.getEpisode(int(result['season']), int(result['episode'])) ep_obj.wantedQuality = [i for i in (wanted_qualities, initial_qualities)[not_downloaded] if cur_quality < i] + # in case we don't want any quality for this episode, skip the episode + if 0 == len(ep_obj.wantedQuality): + logger.log('Dropped episode, no wanted quality for %sx%s: [%s]' % ( + ep_obj.season, ep_obj.episode, ep_obj.show.name), logger.ERROR) + continue ep_obj.eps_aired_in_season = ep_count.get(helpers.tryInt(result['season']), 0) ep_obj.eps_aired_in_scene_season = ep_count_scene.get( helpers.tryInt(result['scene_season']), 0) if result['scene_season'] else ep_obj.eps_aired_in_season @@ -457,7 +469,7 @@ def search_for_needed_episodes(episodes): threading.currentThread().name = orig_thread_name if not len(providers): - logger.log('No NZB/Torrent sources enabled in Search Provider options to do recent searches', logger.WARNING) + logger.log('No NZB/Torrent providers in Media Providers/Options are enabled to match recent episodes', logger.WARNING) elif not search_done: logger.log('Failed recent search of %s enabled provider%s. More info in debug log.' % ( len(providers), helpers.maybe_plural(len(providers))), logger.ERROR) @@ -465,7 +477,7 @@ def search_for_needed_episodes(episodes): return found_results.values() -def search_providers(show, episodes, manual_search=False, torrent_only=False, try_other_searches=False): +def search_providers(show, episodes, manual_search=False, torrent_only=False, try_other_searches=False, old_status=None, scheduled=False): found_results = {} final_results = [] @@ -473,8 +485,17 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr orig_thread_name = threading.currentThread().name + use_quality_list = None + if any([episodes]): + old_status = old_status or failed_history.find_old_status(episodes[0]) or episodes[0].status + if old_status: + status, quality = Quality.splitCompositeStatus(old_status) + use_quality_list = (status not in ( + common.WANTED, common.FAILED, common.UNAIRED, common.SKIPPED, common.IGNORED, common.UNKNOWN)) + provider_list = [x for x in sickbeard.providers.sortedProviderList() if x.is_active() and x.enable_backlog and - (not torrent_only or x.providerType == GenericProvider.TORRENT)] + (not torrent_only or x.providerType == GenericProvider.TORRENT) and + (not scheduled or x.enable_scheduled_backlog)] for cur_provider in provider_list: if cur_provider.anime_only and not show.is_anime: logger.log(u'%s is not an anime, skipping' % show.name, logger.DEBUG) @@ -509,7 +530,7 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr break except Exception as e: logger.log(u'Error while searching %s, skipping: %s' % (cur_provider.name, ex(e)), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) break finally: threading.currentThread().name = orig_thread_name @@ -522,7 +543,8 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr # skip non-tv crap search_results[cur_ep] = filter( lambda ep_item: show_name_helpers.pass_wordlist_checks( - ep_item.name, parse=False) and ep_item.show == show, search_results[cur_ep]) + ep_item.name, parse=False, indexer_lookup=False) and + ep_item.show == show, search_results[cur_ep]) if cur_ep in found_results: found_results[provider_id][cur_ep] += search_results[cur_ep] @@ -605,7 +627,7 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr individual_results = filter( lambda r: show_name_helpers.pass_wordlist_checks( - r.name, parse=False) and r.show == show, individual_results) + r.name, parse=False, indexer_lookup=False) and r.show == show, individual_results) for cur_result in individual_results: if 1 == len(cur_result.episodes): @@ -642,11 +664,11 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr if MULTI_EP_RESULT in found_results[provider_id]: for multi_result in found_results[provider_id][MULTI_EP_RESULT]: - logger.log(u'Checking usefulness of multi episode result %s' % multi_result.name, logger.DEBUG) + logger.log(u'Checking usefulness of multi episode result [%s]' % multi_result.name, logger.DEBUG) - if sickbeard.USE_FAILED_DOWNLOADS and failed_history.hasFailed(multi_result.name, multi_result.size, - multi_result.provider.name): - logger.log(u'%s has previously failed, rejecting this multi episode result' % multi_result.name) + if sickbeard.USE_FAILED_DOWNLOADS and failed_history.has_failed(multi_result.name, multi_result.size, + multi_result.provider.name): + logger.log(u'Rejecting previously failed multi episode result [%s]' % multi_result.name) continue # see how many of the eps that this result covers aren't covered by single results @@ -701,6 +723,7 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr # of all the single ep results narrow it down to the best one for each episode final_results += set(multi_results.values()) + quality_list = use_quality_list and (None, best_qualities)[any(best_qualities)] or None for cur_ep in found_results[provider_id]: if cur_ep in (MULTI_EP_RESULT, SEASON_RESULT): continue @@ -708,7 +731,7 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr if 0 == len(found_results[provider_id][cur_ep]): continue - best_result = pick_best_result(found_results[provider_id][cur_ep], show) + best_result = pick_best_result(found_results[provider_id][cur_ep], show, quality_list) # if all results were rejected move on to the next episode if not best_result: @@ -720,29 +743,38 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr if 'blackhole' != sickbeard.TORRENT_METHOD: best_result.content = None else: - td = best_result.provider.get_url(best_result.url) - if not td: + cache_file = ek.ek(os.path.join, sickbeard.CACHE_DIR or helpers._getTempDir(), + '%s.torrent' % (helpers.sanitizeFileName(best_result.name))) + if not helpers.download_file(best_result.url, cache_file, session=best_result.provider.session): continue + + try: + with open(cache_file, 'rb') as fh: + td = fh.read() + setattr(best_result, 'cache_file', cache_file) + except (StandardError, Exception): + continue + if getattr(best_result.provider, 'chk_td', None): name = None try: hdr = re.findall('(\w+(\d+):)', td[0:6])[0] x, v = len(hdr[0]), int(hdr[1]) - for item in range(0, 12): + while x < len(td): y = x + v name = 'name' == td[x: y] - w = re.findall('((?:i\d+e|d|l)?(\d+):)', td[y: y + 32])[0] + w = re.findall('((?:i-?\d+e|e+|d|l+)*(\d+):)', td[y: y + 32])[0] x, v = y + len(w[0]), int(w[1]) if name: name = td[x: x + v] break - except: + except (StandardError, Exception): continue if name: if not pass_show_wordlist_checks(name, show): continue - if not show_name_helpers.pass_wordlist_checks(name): - logger.log(u'Ignored: %s (debug log has detail)' % name) + if not show_name_helpers.pass_wordlist_checks(name, indexer_lookup=False): + logger.log('Ignored: %s (debug log has detail)' % name) continue best_result.name = name @@ -773,9 +805,11 @@ def search_providers(show, episodes, manual_search=False, torrent_only=False, tr break if not len(provider_list): - logger.log('No NZB/Torrent sources enabled in Search Provider options to do backlog searches', logger.WARNING) + logger.log('No NZB/Torrent providers in Media Providers/Options are allowed for active searching', logger.WARNING) elif not search_done: - logger.log('Failed backlog search of %s enabled provider%s. More info in debug log.' % ( + logger.log('Failed active search of %s enabled provider%s. More info in debug log.' % ( len(provider_list), helpers.maybe_plural(len(provider_list))), logger.ERROR) + elif not any(final_results): + logger.log('No suitable candidates') return final_results diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py index 7f389ce0..31b8334a 100644 --- a/sickbeard/search_queue.py +++ b/sickbeard/search_queue.py @@ -191,39 +191,27 @@ class RecentSearchQueueItem(generic_queue.QueueItem): show_list = sickbeard.showList from_date = datetime.date.fromordinal(1) - need_anime = need_sports = need_sd = need_hd = need_uhd = False - max_sd = Quality.SDDVD - hd_qualities = [Quality.HDTV, Quality.FULLHDTV, Quality.HDWEBDL, Quality.FULLHDWEBDL, - Quality.HDBLURAY, Quality.FULLHDBLURAY] - max_hd = Quality.FULLHDBLURAY + needed = common.neededQualities() for curShow in show_list: if curShow.paused: continue wanted_eps = wanted_episodes(curShow, from_date, unaired=sickbeard.SEARCH_UNAIRED) - if wanted_eps: - if not need_anime and curShow.is_anime: - need_anime = True - if not need_sports and curShow.is_sports: - need_sports = True - if not need_sd or not need_hd or not need_uhd: - for w in wanted_eps: - if need_sd and need_hd and need_uhd: - break - if not w.show.is_anime and not w.show.is_sports: - if Quality.UNKNOWN in w.wantedQuality: - need_sd = need_hd = need_uhd = True - else: - if not need_sd and max_sd >= min(w.wantedQuality): - need_sd = True - if not need_hd and any(i in hd_qualities for i in w.wantedQuality): - need_hd = True - if not need_uhd and max_hd < max(w.wantedQuality): - need_uhd = True - self.episodes.extend(wanted_eps) - self.update_providers(need_anime=need_anime, need_sports=need_sports, - need_sd=need_sd, need_hd=need_hd, need_uhd=need_uhd) + if wanted_eps: + if not needed.all_needed: + if not needed.all_types_needed: + needed.check_needed_types(curShow) + if not needed.all_qualities_needed: + for w in wanted_eps: + if needed.all_qualities_needed: + break + if not w.show.is_anime and not w.show.is_sports: + needed.check_needed_qualities(w.wantedQuality) + + self.episodes.extend(wanted_eps) + + self.update_providers(needed=needed) if not self.episodes: logger.log(u'No search of cache for episodes required') @@ -248,8 +236,8 @@ class RecentSearchQueueItem(generic_queue.QueueItem): helpers.cpu_sleep() - except Exception: - logger.log(traceback.format_exc(), logger.DEBUG) + except (StandardError, Exception): + logger.log(traceback.format_exc(), logger.ERROR) if None is self.success: self.success = False @@ -270,8 +258,9 @@ class RecentSearchQueueItem(generic_queue.QueueItem): cur_time = datetime.datetime.now(network_timezones.sb_timezone) my_db = db.DBConnection() - sql_results = my_db.select('SELECT * FROM tv_episodes WHERE status = ? AND season > 0 AND airdate <= ? AND airdate > 1', - [common.UNAIRED, cur_date]) + sql_results = my_db.select( + 'SELECT * FROM tv_episodes WHERE status = ? AND season > 0 AND airdate <= ? AND airdate > 1', + [common.UNAIRED, cur_date]) sql_l = [] show = None @@ -296,7 +285,7 @@ class RecentSearchQueueItem(generic_queue.QueueItem): # filter out any episodes that haven't aired yet if end_time > cur_time: continue - except: + except (StandardError, Exception): # if an error occurred assume the episode hasn't aired yet continue @@ -318,7 +307,7 @@ class RecentSearchQueueItem(generic_queue.QueueItem): logger.log(u'Found new episodes marked wanted') @staticmethod - def update_providers(need_anime=True, need_sports=True, need_sd=True, need_hd=True, need_uhd=True): + def update_providers(needed=common.neededQualities(need_all=True)): orig_thread_name = threading.currentThread().name threads = [] @@ -332,14 +321,13 @@ class RecentSearchQueueItem(generic_queue.QueueItem): # spawn a thread for each provider to save time waiting for slow response providers threads.append(threading.Thread(target=cur_provider.cache.updateCache, - kwargs={'need_anime': need_anime, 'need_sports': need_sports, - 'need_sd': need_sd, 'need_hd': need_hd, 'need_uhd': need_uhd}, + kwargs={'needed': needed}, name='%s :: [%s]' % (orig_thread_name, cur_provider.name))) # start the thread we just created threads[-1].start() if not len(providers): - logger.log('No NZB/Torrent sources enabled in Search Provider options for cache update', logger.WARNING) + logger.log('No NZB/Torrent providers in Media Providers/Options are enabled to match recent episodes', logger.WARNING) if threads: # wait for all threads to finish @@ -396,8 +384,8 @@ class ManualSearchQueueItem(generic_queue.QueueItem): logger.log(u'Unable to find a download for: [%s]' % self.segment.prettyName()) - except Exception: - logger.log(traceback.format_exc(), logger.DEBUG) + except (StandardError, Exception): + logger.log(traceback.format_exc(), logger.ERROR) finally: # Keep a list with the 100 last executed searches @@ -425,11 +413,13 @@ class BacklogQueueItem(generic_queue.QueueItem): def run(self): generic_queue.QueueItem.run(self) + is_error = False try: logger.log(u'Beginning backlog search for: [%s]' % self.show.name) search_result = search.search_providers( self.show, self.segment, False, - try_other_searches=(not self.standard_backlog or not self.limited_backlog)) + try_other_searches=(not self.standard_backlog or not self.limited_backlog), + scheduled=self.standard_backlog) if search_result: for result in search_result: @@ -440,10 +430,12 @@ class BacklogQueueItem(generic_queue.QueueItem): helpers.cpu_sleep() else: logger.log(u'No needed episodes found during backlog search for: [%s]' % self.show.name) - except Exception: - logger.log(traceback.format_exc(), logger.DEBUG) + except (StandardError, Exception): + is_error = True + logger.log(traceback.format_exc(), logger.ERROR) finally: + logger.log('Completed backlog search %sfor: [%s]' % (('', 'with a debug error ')[is_error], self.show.name)) self.finish() @@ -462,21 +454,23 @@ class FailedQueueItem(generic_queue.QueueItem): self.started = True try: - for epObj in self.segment: + for ep_obj in self.segment: - logger.log(u'Marking episode as bad: [%s]' % epObj.prettyName()) + logger.log(u'Marking episode as bad: [%s]' % ep_obj.prettyName()) - failed_history.markFailed(epObj) + cur_status = ep_obj.status - (release, provider) = failed_history.findRelease(epObj) + failed_history.set_episode_failed(ep_obj) + (release, provider) = failed_history.find_release(ep_obj) + failed_history.revert_episode(ep_obj) if release: - failed_history.logFailed(release) - history.logFailed(epObj, release, provider) + failed_history.add_failed(release) + history.log_failed(ep_obj, release, provider) - failed_history.revertEpisode(epObj) - logger.log(u'Beginning failed download search for: [%s]' % epObj.prettyName()) + logger.log(u'Beginning failed download search for: [%s]' % ep_obj.prettyName()) - search_result = search.search_providers(self.show, self.segment, True, try_other_searches=True) + search_result = search.search_providers( + self.show, self.segment, True, try_other_searches=True, old_status=cur_status) if search_result: for result in search_result: @@ -488,8 +482,8 @@ class FailedQueueItem(generic_queue.QueueItem): else: pass # logger.log(u'No valid episode found to retry for: [%s]' % self.segment.prettyName()) - except Exception: - logger.log(traceback.format_exc(), logger.DEBUG) + except (StandardError, Exception): + logger.log(traceback.format_exc(), logger.ERROR) finally: # Keep a list with the 100 last executed searches diff --git a/sickbeard/show_name_helpers.py b/sickbeard/show_name_helpers.py index a354b856..aa6568f1 100644 --- a/sickbeard/show_name_helpers.py +++ b/sickbeard/show_name_helpers.py @@ -31,7 +31,7 @@ from sickbeard import encodingKludge as ek from name_parser.parser import NameParser, InvalidNameException, InvalidShowException -def pass_wordlist_checks(name, parse=True): +def pass_wordlist_checks(name, parse=True, indexer_lookup=True): """ Filters out non-english and just all-around stupid releases by comparing the word list contents at boundaries or the end of name. @@ -44,7 +44,7 @@ def pass_wordlist_checks(name, parse=True): if parse: err_msg = u'Unable to parse the filename %s into a valid ' % name try: - NameParser().parse(name) + NameParser(indexer_lookup=indexer_lookup).parse(name) except InvalidNameException: logger.log(err_msg + 'episode', logger.DEBUG) return False diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py index 9e5feae3..7a6965a9 100644 --- a/sickbeard/show_queue.py +++ b/sickbeard/show_queue.py @@ -19,16 +19,19 @@ from __future__ import with_statement import traceback +import os import sickbeard -from sickbeard.common import SKIPPED, WANTED, UNAIRED +from sickbeard.common import SKIPPED, WANTED, UNAIRED, statusStrings from sickbeard.tv import TVShow from sickbeard import exceptions, logger, ui, db from sickbeard import generic_queue from sickbeard import name_cache from sickbeard.exceptions import ex +from sickbeard.helpers import should_delete_episode from sickbeard.blackandwhitelist import BlackAndWhiteList +from sickbeard import encodingKludge as ek class ShowQueue(generic_queue.GenericQueue): @@ -174,10 +177,10 @@ class ShowQueue(generic_queue.GenericQueue): def addShow(self, indexer, indexer_id, showDir, default_status=None, quality=None, flatten_folders=None, lang='en', subtitles=None, anime=None, scene=None, paused=None, blacklist=None, whitelist=None, - wanted_begin=None, wanted_latest=None, tag=None): + wanted_begin=None, wanted_latest=None, tag=None, new_show=False): queueItemObj = QueueItemAdd(indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime, scene, paused, blacklist, whitelist, - wanted_begin, wanted_latest, tag) + wanted_begin, wanted_latest, tag, new_show=new_show) self.add_item(queueItemObj) @@ -234,7 +237,8 @@ class ShowQueueItem(generic_queue.QueueItem): class QueueItemAdd(ShowQueueItem): def __init__(self, indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime, - scene, paused, blacklist, whitelist, default_wanted_begin, default_wanted_latest, tag, scheduled_update=False): + scene, paused, blacklist, whitelist, default_wanted_begin, default_wanted_latest, tag, + scheduled_update=False, new_show=False): self.indexer = indexer self.indexer_id = indexer_id @@ -252,6 +256,7 @@ class QueueItemAdd(ShowQueueItem): self.blacklist = blacklist self.whitelist = whitelist self.tag = tag + self.new_show = new_show self.show = None @@ -369,7 +374,7 @@ class QueueItemAdd(ShowQueueItem): except Exception as e: logger.log('Error trying to add show: %s' % ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) self._finishEarly() raise @@ -379,7 +384,7 @@ class QueueItemAdd(ShowQueueItem): self.show.saveToDB() except Exception as e: logger.log('Error saving the show to the database: %s' % ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) self._finishEarly() raise @@ -392,13 +397,13 @@ class QueueItemAdd(ShowQueueItem): logger.log( 'Error with %s, not creating episode list: %s' % (sickbeard.indexerApi(self.show.indexer).name, ex(e)), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) try: self.show.loadEpisodesFromDir() except Exception as e: logger.log('Error searching directory for episodes: %s' % ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) # if they gave a custom status then change all the eps to it my_db = db.DBConnection() @@ -481,9 +486,16 @@ class QueueItemAdd(ShowQueueItem): self.finish() def _finishEarly(self): - if self.show != None: + if self.show is not None: self.show.deleteShow() + if self.new_show: + # if we adding a new show, delete the empty folder that was already created + try: + ek.ek(os.rmdir, self.showDir) + except (StandardError, Exception): + pass + self.finish() @@ -615,8 +627,8 @@ class QueueItemUpdate(ShowQueueItem): try: self.show.saveToDB() except Exception as e: - logger.log('Error saving the episode to the database: %s' % ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log('Error saving the show to the database: %s' % ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # get episode list from DB logger.log('Loading all episodes from the database', logger.DEBUG) @@ -631,9 +643,12 @@ class QueueItemUpdate(ShowQueueItem): (sickbeard.indexerApi(self.show.indexer).name, ex(e)), logger.ERROR) IndexerEpList = None - if IndexerEpList == None: - logger.log('No data returned from %s, unable to update this show' % - sickbeard.indexerApi(self.show.indexer).name, logger.ERROR) + if None is IndexerEpList: + logger.log('No data returned from %s, unable to update episodes for show: %s' % + (sickbeard.indexerApi(self.show.indexer).name, self.show.name), logger.ERROR) + elif not IndexerEpList or 0 == len(IndexerEpList): + logger.log('No episodes returned from %s for show: %s' % + (sickbeard.indexerApi(self.show.indexer).name, self.show.name), logger.WARNING) else: # for each ep we found on TVDB delete it from the DB list for curSeason in IndexerEpList: @@ -645,13 +660,18 @@ class QueueItemUpdate(ShowQueueItem): # for the remaining episodes in the DB list just delete them from the DB for curSeason in DBEpList: for curEpisode in DBEpList[curSeason]: - logger.log('Permanently deleting episode %sx%s from the database' % - (curSeason, curEpisode), logger.MESSAGE) curEp = self.show.getEpisode(curSeason, curEpisode) - try: - curEp.deleteEpisode() - except exceptions.EpisodeDeletedException: - pass + status = sickbeard.common.Quality.splitCompositeStatus(curEp.status)[0] + if should_delete_episode(status): + logger.log('Permanently deleting episode %sx%s from the database' % + (curSeason, curEpisode), logger.MESSAGE) + try: + curEp.deleteEpisode() + except exceptions.EpisodeDeletedException: + pass + else: + logger.log('Not deleting episode %sx%s from the database because status is: %s' % + (curSeason, curEpisode, statusStrings[status]), logger.MESSAGE) if self.priority != generic_queue.QueuePriorities.NORMAL: self.kwargs['priority'] = self.priority diff --git a/sickbeard/show_updater.py b/sickbeard/show_updater.py index 4e9320ba..8069fca5 100644 --- a/sickbeard/show_updater.py +++ b/sickbeard/show_updater.py @@ -17,6 +17,7 @@ # along with SickGear. If not, see . import datetime +import traceback import sickbeard from sickbeard import logger, exceptions, ui, db, network_timezones, failed_history @@ -36,36 +37,68 @@ class ShowUpdater: update_date = update_datetime.date() # refresh network timezones - network_timezones.update_network_dict() + try: + network_timezones.update_network_dict() + except Exception: + logger.log('network timezone update error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # update xem id lists - sickbeard.scene_exceptions.get_xem_ids() + try: + sickbeard.scene_exceptions.get_xem_ids() + except Exception: + logger.log('xem id list update error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # update scene exceptions - sickbeard.scene_exceptions.retrieve_exceptions() + try: + sickbeard.scene_exceptions.retrieve_exceptions() + except Exception: + logger.log('scene exceptions update error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # sure, why not? if sickbeard.USE_FAILED_DOWNLOADS: - failed_history.trimHistory() + try: + failed_history.remove_old_history() + except Exception: + logger.log('Failed History cleanup error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # clear the data of unused providers - sickbeard.helpers.clear_unused_providers() + try: + sickbeard.helpers.clear_unused_providers() + except Exception: + logger.log('unused provider cleanup error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # cleanup image cache - sickbeard.helpers.cleanup_cache() + try: + sickbeard.helpers.cleanup_cache() + except Exception: + logger.log('image cache cleanup error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # add missing mapped ids if not sickbeard.background_mapping_task.is_alive(): logger.log(u'Updating the Indexer mappings') import threading - sickbeard.background_mapping_task = threading.Thread( - name='LOAD-MAPPINGS', target=sickbeard.indexermapper.load_mapped_ids, kwargs={'update': True}) - sickbeard.background_mapping_task.start() + try: + sickbeard.background_mapping_task = threading.Thread( + name='LOAD-MAPPINGS', target=sickbeard.indexermapper.load_mapped_ids, kwargs={'update': True}) + sickbeard.background_mapping_task.start() + except Exception: + logger.log('missing mapped ids update error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) logger.log(u'Doing full update on all shows') # clean out cache directory, remove everything > 12 hours old - sickbeard.helpers.clearCache() + try: + sickbeard.helpers.clearCache() + except Exception: + logger.log('cache dir cleanup error', logger.ERROR) + logger.log(traceback.format_exc(), logger.ERROR) # select 10 'Ended' tv_shows updated more than 90 days ago # and all shows not updated more then 180 days ago to include in this update diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 634d4259..c8df4988 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -36,6 +36,8 @@ from name_parser.parser import NameParser, InvalidNameException, InvalidShowExce from lib import subliminal import fnmatch +from imdb._exceptions import IMDbError + try: from lib.send2trash import send2trash except ImportError: @@ -52,6 +54,7 @@ from sickbeard import postProcessor from sickbeard import subtitles from sickbeard import history from sickbeard import network_timezones +from sickbeard.sbdatetime import sbdatetime from sickbeard.blackandwhitelist import BlackAndWhiteList from sickbeard.indexermapper import del_mapping, save_mapping, MapStatus from sickbeard.generic_queue import QueuePriorities @@ -59,21 +62,33 @@ from sickbeard.generic_queue import QueuePriorities from sickbeard import encodingKludge as ek from common import Quality, Overview, statusStrings -from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, ARCHIVED, IGNORED, UNAIRED, WANTED, SKIPPED, \ - UNKNOWN, FAILED, SUBTITLED +from common import SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, SNATCHED_ANY, DOWNLOADED, ARCHIVED, \ + IGNORED, UNAIRED, WANTED, SKIPPED, UNKNOWN, FAILED, SUBTITLED from common import NAMING_DUPLICATE, NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_SEPARATED_REPEAT, \ NAMING_LIMITED_EXTEND_E_PREFIXED +concurrent_show_not_found_days = 7 +show_not_found_retry_days = 7 -def dirty_setter(attr_name): + +def dirty_setter(attr_name, types=None): def wrapper(self, val): if getattr(self, attr_name) != val: - setattr(self, attr_name, val) - self.dirty = True + if None is types or isinstance(val, types): + setattr(self, attr_name, val) + self.dirty = True + else: + logger.log('Didn\'t change property "%s" because expected: %s, but got: %s with value: %s' % + (attr_name, types, type(val), val), logger.WARNING) return wrapper +def dict_prevent_None(d, key, default): + v = getattr(d, key, default) + return (v, default)[None is v] + + class TVShow(object): def __init__(self, indexer, indexerid, lang=''): self._indexerid = int(indexerid) @@ -105,6 +120,8 @@ class TVShow(object): self._overview = '' self._tag = '' self._mapped_ids = {} + self._not_found_count = None + self._last_found_on_indexer = -1 self.dirty = True @@ -136,7 +153,6 @@ class TVShow(object): status = property(lambda self: self._status, dirty_setter('_status')) airs = property(lambda self: self._airs, dirty_setter('_airs')) startyear = property(lambda self: self._startyear, dirty_setter('_startyear')) - paused = property(lambda self: self._paused, dirty_setter('_paused')) air_by_date = property(lambda self: self._air_by_date, dirty_setter('_air_by_date')) subtitles = property(lambda self: self._subtitles, dirty_setter('_subtitles')) dvdorder = property(lambda self: self._dvdorder, dirty_setter('_dvdorder')) @@ -151,6 +167,78 @@ class TVShow(object): overview = property(lambda self: self._overview, dirty_setter('_overview')) tag = property(lambda self: self._tag, dirty_setter('_tag')) + def _helper_load_failed_db(self): + if None is self._not_found_count or self._last_found_on_indexer == -1: + myDB = db.DBConnection() + results = myDB.select('SELECT fail_count, last_success FROM tv_shows_not_found WHERE indexer = ? AND indexer_id = ?', + [self.indexer, self.indexerid]) + if results: + self._not_found_count = helpers.tryInt(results[0]['fail_count']) + self._last_found_on_indexer = helpers.tryInt(results[0]['last_success']) + else: + self._not_found_count = 0 + self._last_found_on_indexer = 0 + + @property + def not_found_count(self): + self._helper_load_failed_db() + return self._not_found_count + + @not_found_count.setter + def not_found_count(self, v): + if isinstance(v, (int, long)) and v != self._not_found_count: + self._last_found_on_indexer = self.last_found_on_indexer + myDB = db.DBConnection() + # noinspection PyUnresolvedReferences + last_check = sbdatetime.now().totimestamp(default=0) + # in case of flag change (+/-) don't change last_check date + if abs(v) == abs(self._not_found_count): + results = myDB.select('SELECT last_check FROM tv_shows_not_found WHERE indexer = ? AND indexer_id = ?', + [self.indexer, self.indexerid]) + if results: + last_check = helpers.tryInt(results[0]['last_check']) + myDB.upsert('tv_shows_not_found', + {'fail_count': v, 'last_check': last_check, + 'last_success': self._last_found_on_indexer}, + {'indexer': self.indexer, 'indexer_id': self.indexerid}) + self._not_found_count = v + + @property + def last_found_on_indexer(self): + self._helper_load_failed_db() + return (self._last_found_on_indexer, self.last_update_indexer)[self._last_found_on_indexer <= 0] + + def inc_not_found_count(self): + myDB = db.DBConnection() + results = myDB.select('SELECT last_check FROM tv_shows_not_found WHERE indexer = ? AND indexer_id = ?', + [self.indexer, self.indexerid]) + days = (show_not_found_retry_days - 1, 0)[abs(self.not_found_count) <= concurrent_show_not_found_days] + if not results or datetime.datetime.fromtimestamp(helpers.tryInt(results[0]['last_check'])) + \ + datetime.timedelta(days=days, hours=18) < datetime.datetime.now(): + self.not_found_count += (-1, 1)[0 <= self.not_found_count] + + def reset_not_found_count(self): + if 0 != self.not_found_count: + self._not_found_count = 0 + self._last_found_on_indexer = 0 + myDB = db.DBConnection() + myDB.action('DELETE FROM tv_shows_not_found WHERE indexer = ? AND indexer_id = ?', + [self.indexer, self.indexerid]) + + @property + def paused(self): + return self._paused + + @paused.setter + def paused(self, value): + if value != self._paused: + if isinstance(value, bool) or (isinstance(value, (int, long)) and value in [0, 1]): + self._paused = int(value) + self.dirty = True + else: + logger.log('tried to set paused property to invalid value: %s of type: %s' % (value, type(value)), + logger.ERROR) + @property def ids(self): if not self._mapped_ids: @@ -224,37 +312,43 @@ class TVShow(object): self.episodes[curSeason][curEp] = None del myEp - def getAllEpisodes(self, season=None, has_location=False): + def getAllEpisodes(self, season=None, has_location=False, check_related_eps=True): - sql_selection = 'SELECT season, episode, ' + sql_selection = 'SELECT season, episode' - # subselection to detect multi-episodes early, share_location > 0 - sql_selection = sql_selection + ' (SELECT COUNT (*) FROM tv_episodes WHERE showid = tve.showid AND season = tve.season AND location != "" AND location = tve.location AND episode != tve.episode) AS share_location ' + if check_related_eps: + # subselection to detect multi-episodes early, share_location > 0 + sql_selection += ' , (SELECT COUNT (*) FROM tv_episodes WHERE showid = tve.showid AND season = ' \ + 'tve.season AND location != "" AND location = tve.location AND episode != tve.episode) ' \ + 'AS share_location ' - sql_selection = sql_selection + ' FROM tv_episodes tve WHERE showid = ' + str(self.indexerid) + sql_selection += ' FROM tv_episodes tve WHERE indexer = ? AND showid = ?' + sql_parameter = [self.indexer, self.indexerid] if season is not None: - sql_selection = sql_selection + ' AND season = ' + str(season) + sql_selection += ' AND season = ?' + sql_parameter += [season] if has_location: - sql_selection = sql_selection + ' AND location != "" ' + sql_selection += ' AND location != "" ' # need ORDER episode ASC to rename multi-episodes in order S01E01-02 - sql_selection = sql_selection + ' ORDER BY season ASC, episode ASC' + sql_selection += ' ORDER BY season ASC, episode ASC' myDB = db.DBConnection() - results = myDB.select(sql_selection) + results = myDB.select(sql_selection, sql_parameter) ep_list = [] for cur_result in results: cur_ep = self.getEpisode(int(cur_result['season']), int(cur_result['episode'])) if cur_ep: cur_ep.relatedEps = [] - if cur_ep.location: + if check_related_eps and cur_ep.location: # if there is a location, check if it's a multi-episode (share_location > 0) and put them in relatedEps if cur_result['share_location'] > 0: related_eps_result = myDB.select( - 'SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND location = ? AND episode != ? ORDER BY episode ASC', + 'SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND location = ? AND ' + 'episode != ? ORDER BY episode ASC', [self.indexerid, cur_ep.season, cur_ep.location, cur_ep.episode]) for cur_related_ep in related_eps_result: related_ep = self.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) @@ -264,7 +358,6 @@ class TVShow(object): return ep_list - def getEpisode(self, season=None, episode=None, file=None, noCreate=False, absolute_number=None, forceUpdate=False): # if we get an anime get the real season and episode @@ -318,6 +411,13 @@ class TVShow(object): logger.log('Status missing for showid: [%s] with status: [%s]' % (cur_indexerid, self.status), logger.DEBUG) + last_update_indexer = datetime.date.fromordinal(self.last_update_indexer) + + # if show was not found for 1 week, only retry to update once a week + if (concurrent_show_not_found_days < abs(self.not_found_count)) \ + and (update_date - last_update_indexer) < datetime.timedelta(days=show_not_found_retry_days): + return False + myDB = db.DBConnection() sql_result = myDB.mass_action( [['SELECT airdate FROM [tv_episodes] WHERE showid = ? AND season > "0" ORDER BY season DESC, episode DESC LIMIT 1', [cur_indexerid]], @@ -327,8 +427,6 @@ class TVShow(object): last_airdate = datetime.date.fromordinal(sql_result[1][0]['airdate']) if sql_result and sql_result[1] else datetime.date.fromordinal(1) - last_update_indexer = datetime.date.fromordinal(self.last_update_indexer) - # if show is not 'Ended' and last episode aired less then 460 days ago or don't have an airdate for the last episode always update (status 'Continuing' or '') update_days_limit = 2013 ended_limit = datetime.timedelta(days=update_days_limit) @@ -471,7 +569,7 @@ class TVShow(object): curEpisode.refreshSubtitles() except: logger.log('%s: Could not refresh subtitles' % self.indexerid, logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + logger.log(traceback.format_exc(), logger.ERROR) result = curEpisode.get_sql() if None is not result: @@ -484,7 +582,7 @@ class TVShow(object): def loadEpisodesFromDB(self, update=False): - logger.log('Loading all episodes from the DB') + logger.log('Loading all episodes for [%s] from the DB' % self.name) myDB = db.DBConnection() sql = 'SELECT * FROM tv_episodes WHERE showid = ? AND indexer = ?' @@ -518,27 +616,27 @@ class TVShow(object): try: cachedSeasons[curSeason] = cachedShow[curSeason] except sickbeard.indexer_seasonnotfound as e: - logger.log('Error when trying to load the episode from %s: %s' % - (sickbeard.indexerApi(self.indexer).name, e.message), logger.WARNING) + logger.log('Error when trying to load the episode for [%s] from %s: %s' % + (self.name, sickbeard.indexerApi(self.indexer).name, e.message), logger.WARNING) deleteEp = True if not curSeason in scannedEps: scannedEps[curSeason] = {} - logger.log('Loading episode %sx%s from the DB' % (curSeason, curEpisode), logger.DEBUG) + logger.log('Loading episode %sx%s for [%s] from the DB' % (curSeason, curEpisode, self.name), logger.DEBUG) try: curEp = self.getEpisode(curSeason, curEpisode) # if we found out that the ep is no longer on TVDB then delete it from our database too - if deleteEp: + if deleteEp and helpers.should_delete_episode(curEp.status): curEp.deleteEpisode() curEp.loadFromDB(curSeason, curEpisode) curEp.loadFromIndexer(tvapi=t, cachedSeason=cachedSeasons[curSeason], update=update) scannedEps[curSeason][curEpisode] = True except exceptions.EpisodeDeletedException: - logger.log('Tried loading an episode from the DB that should have been deleted, skipping it', + logger.log('Tried loading an episode from [%s] from the DB that should have been deleted, skipping it' % self.name, logger.DEBUG) continue @@ -557,14 +655,14 @@ class TVShow(object): if self.dvdorder != 0: lINDEXER_API_PARMS['dvdorder'] = True - logger.log('%s: Loading all episodes from %s..' % (self.indexerid, sickbeard.indexerApi(self.indexer).name)) + logger.log('%s: Loading all episodes for [%s] from %s..' % (self.indexerid, self.name, sickbeard.indexerApi(self.indexer).name)) try: t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS) showObj = t[self.indexerid] except sickbeard.indexer_error: - logger.log('%s timed out, unable to update episodes from %s' % - (sickbeard.indexerApi(self.indexer).name, sickbeard.indexerApi(self.indexer).name), logger.ERROR) + logger.log('%s timed out, unable to update episodes for [%s] from %s' % + (sickbeard.indexerApi(self.indexer).name, self.name, sickbeard.indexerApi(self.indexer).name), logger.ERROR) return None scannedEps = {} @@ -579,19 +677,19 @@ class TVShow(object): try: ep = self.getEpisode(season, episode) except exceptions.EpisodeNotFoundException: - logger.log('%s: %s object for %sx%s is incomplete, skipping this episode' % - (self.indexerid, sickbeard.indexerApi(self.indexer).name, season, episode)) + logger.log('%s: %s object for %sx%s from [%s] is incomplete, skipping this episode' % + (self.indexerid, sickbeard.indexerApi(self.indexer).name, season, episode, self.name)) continue else: try: ep.loadFromIndexer(tvapi=t, update=update) except exceptions.EpisodeDeletedException: - logger.log('The episode was deleted, skipping the rest of the load') + logger.log('The episode from [%s] was deleted, skipping the rest of the load' % self.name) continue with ep.lock: - logger.log('%s: Loading info from %s for episode %sx%s' % - (self.indexerid, sickbeard.indexerApi(self.indexer).name, season, episode), logger.DEBUG) + logger.log('%s: Loading info from %s for episode %sx%s from [%s]' % + (self.indexerid, sickbeard.indexerApi(self.indexer).name, season, episode, self.name), logger.DEBUG) ep.loadFromIndexer(season, episode, tvapi=t, update=update) result = ep.get_sql() @@ -725,7 +823,7 @@ class TVShow(object): # check for status/quality changes as long as it's a new file elif not same_file and sickbeard.helpers.has_media_ext(file)\ - and cur_ep.status not in Quality.DOWNLOADED + [ARCHIVED, IGNORED]: + and cur_ep.status not in Quality.DOWNLOADED + Quality.ARCHIVED + [IGNORED]: old_status, old_quality = Quality.splitCompositeStatus(cur_ep.status) new_quality = Quality.nameQuality(file, self.is_anime) @@ -748,7 +846,7 @@ class TVShow(object): % (Quality.qualityStrings[old_quality], Quality.qualityStrings[new_quality]), logger.DEBUG) new_status = DOWNLOADED - elif old_status not in (SNATCHED, SNATCHED_PROPER): + elif old_status not in SNATCHED_ANY: new_status = DOWNLOADED if None is not new_status: @@ -779,10 +877,10 @@ class TVShow(object): sqlResults = myDB.select('SELECT * FROM tv_shows WHERE indexer_id = ?', [self.indexerid]) if len(sqlResults) > 1: - logger.log('%s: Loading show info from database' % self.indexerid) + logger.log('%s: Loading show info [%s] from database' % (self.indexerid, self.name)) raise exceptions.MultipleDBShowsException() elif len(sqlResults) == 0: - logger.log('%s: Unable to find the show in the database' % self.indexerid) + logger.log('%s: Unable to find the show [%s] in the database' % (self.indexerid, self.name)) return else: if not self.indexer: @@ -889,7 +987,7 @@ class TVShow(object): def loadFromIndexer(self, cache=True, tvapi=None, cachedSeason=None): - logger.log('%s: Loading show info from %s' % (self.indexerid, sickbeard.indexerApi(self.indexer).name)) + logger.log('%s: Loading show info [%s] from %s' % (self.indexerid, self.name, sickbeard.indexerApi(self.indexer).name)) # There's gotta be a better way of doing this but we don't wanna # change the cache value elsewhere @@ -912,8 +1010,13 @@ class TVShow(object): myEp = t[self.indexerid, False] if None is myEp: - logger.log('Show not found (maybe even removed?)', logger.WARNING) + if hasattr(t, 'show_not_found') and t.show_not_found: + self.inc_not_found_count() + logger.log('Show [%s] not found (maybe even removed?)' % self.name, logger.WARNING) + else: + logger.log('Show data [%s] not found' % self.name, logger.WARNING) return False + self.reset_not_found_count() try: self.name = myEp['seriesname'].strip() @@ -921,12 +1024,12 @@ class TVShow(object): raise sickbeard.indexer_attributenotfound( "Found %s, but attribute 'seriesname' was empty." % (self.indexerid)) - self.classification = getattr(myEp, 'classification', 'Scripted') - self.genre = getattr(myEp, 'genre', '') - self.network = getattr(myEp, 'network', '') - self.runtime = getattr(myEp, 'runtime', '') + self.classification = dict_prevent_None(myEp, 'classification', 'Scripted') + self.genre = dict_prevent_None(myEp, 'genre', '') + self.network = dict_prevent_None(myEp, 'network', '') + self.runtime = dict_prevent_None(myEp, 'runtime', '') - self.imdbid = getattr(myEp, 'imdb_id', '') + self.imdbid = dict_prevent_None(myEp, 'imdb_id', '') if getattr(myEp, 'airs_dayofweek', None) is not None and getattr(myEp, 'airs_time', None) is not None: self.airs = ('%s %s' % (myEp['airs_dayofweek'], myEp['airs_time'])).strip() @@ -934,8 +1037,8 @@ class TVShow(object): if getattr(myEp, 'firstaired', None) is not None: self.startyear = int(str(myEp["firstaired"]).split('-')[0]) - self.status = getattr(myEp, 'status', '') - self.overview = getattr(myEp, 'overview', '') + self.status = dict_prevent_None(myEp, 'status', '') + self.overview = dict_prevent_None(myEp, 'overview', '') def load_imdb_info(self): @@ -944,7 +1047,7 @@ class TVShow(object): from lib.imdb import _exceptions as imdb_exceptions - logger.log('Retrieving show info from IMDb', logger.DEBUG) + logger.log('Retrieving show info [%s] from IMDb' % self.name, logger.DEBUG) try: self._get_imdb_info() except imdb_exceptions.IMDbDataAccessError as e: @@ -953,7 +1056,7 @@ class TVShow(object): logger.log('Something is wrong with IMDb api: %s' % ex(e), logger.WARNING) except Exception as e: logger.log('Error loading IMDb info: %s' % ex(e), logger.ERROR) - logger.log('%s' % traceback.format_exc(), logger.DEBUG) + logger.log('%s' % traceback.format_exc(), logger.ERROR) def _get_imdb_info(self): @@ -973,8 +1076,12 @@ class TVShow(object): 'votes': '', 'last_update': ''} - i = imdb.IMDb() - imdbTv = i.get_movie(str(re.sub('[^0-9]', '', self.imdbid or '%07d' % self.ids[indexermapper.INDEXER_IMDB]['id']))) + try: + i = imdb.IMDb() + imdbTv = i.get_movie( + str(re.sub('[^0-9]', '', self.imdbid or '%07d' % self.ids[indexermapper.INDEXER_IMDB]['id']))) + except IMDbError: + return for key in filter(lambda x: x.replace('_', ' ') in imdbTv.keys(), imdb_info.keys()): # Store only the first value for string type @@ -1029,7 +1136,7 @@ class TVShow(object): logger.log('%s: Parsed latest IMDb show info for [%s]' % (self.indexerid, self.name)) def nextEpisode(self): - logger.log('%s: Finding the episode which airs next' % self.indexerid, logger.DEBUG) + logger.log('%s: Finding the episode which airs next for: %s' % (self.indexerid, self.name), logger.DEBUG) curDate = datetime.date.today().toordinal() if not self.nextaired or self.nextaired and curDate > self.nextaired: @@ -1057,7 +1164,8 @@ class TVShow(object): ["DELETE FROM scene_numbering WHERE indexer_id = ? AND indexer = ?", [self.indexerid, self.indexer]], ["DELETE FROM whitelist WHERE show_id = ?", [self.indexerid]], ["DELETE FROM blacklist WHERE show_id = ?", [self.indexerid]], - ["DELETE FROM indexer_mapping WHERE indexer_id = ? AND indexer = ?", [self.indexerid, self.indexer]]] + ["DELETE FROM indexer_mapping WHERE indexer_id = ? AND indexer = ?", [self.indexerid, self.indexer]], + ["DELETE FROM tv_shows_not_found WHERE indexer = ? AND indexer_id = ?", [self.indexer, self.indexerid]]] myDB = db.DBConnection() myDB.mass_action(sql_l) @@ -1135,7 +1243,7 @@ class TVShow(object): self.loadEpisodesFromDir() # run through all locations from DB, check that they exist - logger.log('%s: Loading all episodes with a location from the database' % self.indexerid) + logger.log('%s: Loading all episodes for [%s] with a location from the database' % (self.indexerid, self.name)) myDB = db.DBConnection() sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != ''", [self.indexerid]) @@ -1149,7 +1257,7 @@ class TVShow(object): try: curEp = self.getEpisode(season, episode) except exceptions.EpisodeDeletedException: - logger.log('The episode was deleted while we were refreshing it, moving on to the next one', + logger.log('The episode from [%s] was deleted while we were refreshing it, moving on to the next one' % self.name, logger.DEBUG) continue @@ -1162,7 +1270,11 @@ class TVShow(object): with curEp.lock: # if it used to have a file associated with it and it doesn't anymore then set it to IGNORED if curEp.location and curEp.status in Quality.DOWNLOADED: - curEp.status = (sickbeard.SKIP_REMOVED_FILES, IGNORED)[not sickbeard.SKIP_REMOVED_FILES] + if ARCHIVED == sickbeard.SKIP_REMOVED_FILES: + curEp.status = Quality.compositeStatus( + ARCHIVED, Quality.qualityDownloaded(curEp.status)) + else: + curEp.status = (sickbeard.SKIP_REMOVED_FILES, IGNORED)[not sickbeard.SKIP_REMOVED_FILES] logger.log('%s: File no longer at location for s%02de%02d, episode removed and status changed to %s' % (str(self.indexerid), season, episode, statusStrings[curEp.status]), logger.DEBUG) @@ -1203,7 +1315,7 @@ class TVShow(object): episode = self.makeEpFromFile(episodeLoc['location']) subtitles = episode.downloadSubtitles(force=force) except Exception as e: - logger.log('Error occurred when downloading subtitles: %s' % traceback.format_exc(), logger.DEBUG) + logger.log('Error occurred when downloading subtitles: %s' % traceback.format_exc(), logger.ERROR) return def switchIndexer(self, old_indexer, old_indexerid, pausestatus_after=None): @@ -1220,13 +1332,15 @@ class TVShow(object): [self.indexer, self.indexerid, old_indexer, old_indexerid]], ['UPDATE whitelist SET show_id = ? WHERE show_id = ?', [self.indexerid, old_indexerid]], ['UPDATE xem_refresh SET indexer = ?, indexer_id = ? WHERE indexer = ? AND indexer_id = ?', - [self.indexer, self.indexerid, old_indexer, old_indexerid]]]) + [self.indexer, self.indexerid, old_indexer, old_indexerid]], + ['DELETE FROM tv_shows_not_found WHERE indexer = ? AND indexer_id = ?', [old_indexer, old_indexerid]]]) myFailedDB = db.DBConnection('failed.db') myFailedDB.action('UPDATE history SET showid = ? WHERE showid = ?', [self.indexerid, old_indexerid]) del_mapping(old_indexer, old_indexerid) self.ids[old_indexer]['status'] = MapStatus.NONE self.ids[self.indexer]['status'] = MapStatus.SOURCE + self.ids[self.indexer]['id'] = self.indexerid save_mapping(self) name_cache.remove_from_namecache(old_indexerid) @@ -1257,6 +1371,7 @@ class TVShow(object): sickbeard.save_config() name_cache.buildNameCache(self) + self.reset_not_found_count() # force the update try: @@ -1358,7 +1473,7 @@ class TVShow(object): logger.log('Unable to find a matching episode in database, ignoring found episode', logger.DEBUG) return False - epStatus = int(sqlResults[0]["status"]) + epStatus = Quality.splitCompositeStatus(int(sqlResults[0]['status']))[0] epStatus_text = statusStrings[epStatus] logger.log('Existing episode status: %s (%s)' % (statusStrings[epStatus], epStatus_text), logger.DEBUG) @@ -1383,7 +1498,7 @@ class TVShow(object): logger.DEBUG) curStatus, curQuality = Quality.splitCompositeStatus(epStatus) - downloadedStatusList = (DOWNLOADED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST) + downloadedStatusList = SNATCHED_ANY + [DOWNLOADED] # special case: already downloaded quality is not in any of the wanted Qualities if curStatus in downloadedStatusList and curQuality not in allQualities: wantedQualities = allQualities @@ -1413,11 +1528,11 @@ class TVShow(object): return Overview.SKIPPED if status in (UNAIRED, UNKNOWN): return Overview.UNAIRED - if status in [SUBTITLED] + Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.FAILED + Quality.SNATCHED_BEST: + if status in [SUBTITLED] + Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.FAILED: if FAILED == status: return Overview.WANTED - if status in (SNATCHED, SNATCHED_PROPER, SNATCHED_BEST): + if status in SNATCHED_ANY: return Overview.SNATCHED void, best_qualities = Quality.splitQuality(self.quality) @@ -1486,7 +1601,7 @@ class TVEpisode(object): self.wantedQuality = [] - name = property(lambda self: self._name, dirty_setter('_name')) + name = property(lambda self: self._name, dirty_setter('_name', basestring)) season = property(lambda self: self._season, dirty_setter('_season')) episode = property(lambda self: self._episode, dirty_setter('_episode')) absolute_number = property(lambda self: self._absolute_number, dirty_setter('_absolute_number')) @@ -1572,7 +1687,12 @@ class TVEpisode(object): newsubtitles = set(self.subtitles).difference(set(previous_subtitles)) if newsubtitles: - subtitleList = ", ".join(subliminal.language.Language(x).name for x in newsubtitles) + try: + subtitleList = ", ".join(subliminal.language.Language(x).name for x in newsubtitles) + except(StandardError, Exception): + logger.log('Could not parse a language to use to fetch subtitles for episode %sx%s' % + (self.season, self.episode), logger.DEBUG) + return logger.log('%s: Downloaded %s subtitles for episode %sx%s' % (self.show.indexerid, subtitleList, self.season, self.episode), logger.DEBUG) @@ -1585,7 +1705,7 @@ class TVEpisode(object): if sickbeard.SUBTITLES_HISTORY: for video in subtitles: for subtitle in subtitles.get(video): - history.logSubtitle(self.show.indexerid, self.season, self.episode, self.status, subtitle) + history.log_subtitle(self.show.indexerid, self.season, self.episode, self.status, subtitle) return subtitles @@ -1783,7 +1903,7 @@ class TVEpisode(object): logger.log('Unable to find the episode on %s... has it been removed? Should I delete from db?' % sickbeard.indexerApi(self.indexer).name, logger.DEBUG) # if I'm no longer on the Indexers but I once was then delete myself from the DB - if -1 != self.indexerid: + if -1 != self.indexerid and helpers.should_delete_episode(self.status): self.deleteEpisode() return @@ -1795,7 +1915,7 @@ class TVEpisode(object): (self.show.indexerid, season, episode, myEp['absolute_number']), logger.DEBUG) self.absolute_number = int(myEp['absolute_number']) - self.name = getattr(myEp, 'episodename', '') + self.name = dict_prevent_None(myEp, 'episodename', '') self.season = season self.episode = episode @@ -1813,7 +1933,7 @@ class TVEpisode(object): self.season, self.episode ) - self.description = getattr(myEp, 'overview', '') + self.description = dict_prevent_None(myEp, 'overview', '') firstaired = getattr(myEp, 'firstaired', None) if None is firstaired or firstaired in '0000-00-00': @@ -1827,7 +1947,7 @@ class TVEpisode(object): logger.log('Malformed air date retrieved from %s (%s - %sx%s)' % (sickbeard.indexerApi(self.indexer).name, self.show.name, season, episode), logger.ERROR) # if I'm incomplete on TVDB but I once was complete then just delete myself from the DB for now - if -1 != self.indexerid: + if -1 != self.indexerid and helpers.should_delete_episode(self.status): self.deleteEpisode() return False @@ -1835,7 +1955,8 @@ class TVEpisode(object): self.indexerid = getattr(myEp, 'id', None) if None is self.indexerid: logger.log('Failed to retrieve ID from %s' % sickbeard.indexerApi(self.indexer).name, logger.ERROR) - self.deleteEpisode() + if helpers.should_delete_episode(self.status): + self.deleteEpisode() return False # don't update show status if show dir is missing, unless it's missing on purpose @@ -1900,7 +2021,7 @@ class TVEpisode(object): # if we have a media file then it's downloaded elif sickbeard.helpers.has_media_ext(self.location): # leave propers alone, you have to either post-process them or manually change them back - if self.status not in Quality.SNATCHED_PROPER + Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED]: + if self.status not in Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED: msg = '(1) Status changes from %s to ' % statusStrings[self.status] self.status = Quality.statusFromNameOrFile(self.location, anime=self.show.is_anime) logger.log('%s%s' % (msg, statusStrings[self.status]), logger.DEBUG) @@ -2622,26 +2743,25 @@ class TVEpisode(object): % (self.show.indexerid, ek.ek(os.path.basename, self.location)), logger.DEBUG) return - hr = m = 0 - airs = re.search('.*?(\d{1,2})(?::\s*?(\d{2}))?\s*(pm)?', self.show.airs, re.I) - if airs: - hr = int(airs.group(1)) - hr = (12 + hr, hr)[None is airs.group(3)] - hr = (hr, hr - 12)[0 == hr % 12 and 0 != hr] - m = int((airs.group(2), m)[None is airs.group(2)]) + hr, m = network_timezones.parse_time(self.show.airs) airtime = datetime.time(hr, m) - airdatetime = datetime.datetime.combine(self.airdate, airtime) + aired_dt = datetime.datetime.combine(self.airdate, airtime) + try: + aired_epoch = helpers.datetime_to_epoch(aired_dt) + filemtime = int(ek.ek(os.path.getmtime, self.location)) + except (StandardError, Exception): + return - filemtime = datetime.datetime.fromtimestamp(ek.ek(os.path.getmtime, self.location)) + if filemtime != aired_epoch: - if filemtime != airdatetime: - import time + result, loglevel = 'Changed', logger.MESSAGE + if not helpers.touch_file(self.location, aired_epoch): + result, loglevel = 'Error changing', logger.WARNING - airdatetime = airdatetime.timetuple() - if helpers.touchFile(self.location, time.mktime(airdatetime)): - logger.log('%s: Changed modify date of %s to show air date %s' - % (self.show.indexerid, ek.ek(os.path.basename, self.location), time.strftime('%b %d,%Y (%H:%M)', airdatetime))) + logger.log('%s: %s modify date of %s to show air date %s' + % (self.show.indexerid, result, ek.ek(os.path.basename, self.location), + aired_dt.strftime('%b %d,%Y (%H:%M)')), loglevel) def __getstate__(self): d = dict(self.__dict__) diff --git a/sickbeard/tvcache.py b/sickbeard/tvcache.py index f464450d..d7ec58f6 100644 --- a/sickbeard/tvcache.py +++ b/sickbeard/tvcache.py @@ -66,7 +66,7 @@ class TVCache: # override this in the provider if recent search has a different data layout to backlog searches return self.provider._title_and_url(item) - def _cache_data(self): + def _cache_data(self, **kwargs): data = None return data @@ -84,7 +84,7 @@ class TVCache: return [] if self.should_update(): - data = self._cache_data() + data = self._cache_data(**kwargs) # clear cache if data: @@ -208,7 +208,7 @@ class TVCache: return None try: - np = NameParser(showObj=showObj, convert=True) + np = NameParser(showObj=showObj, convert=True, indexer_lookup=False) parse_result = np.parse(name) except InvalidNameException: logger.log(u'Unable to parse the filename ' + name + ' into a valid episode', logger.DEBUG) @@ -258,7 +258,8 @@ class TVCache: def listPropers(self, date=None): myDB = self.get_db() - sql = "SELECT * FROM provider_cache WHERE name LIKE '%.PROPER.%' OR name LIKE '%.REPACK.%' AND provider = ?" + sql = "SELECT * FROM provider_cache WHERE name LIKE '%.PROPER.%' OR name LIKE '%.REPACK.%' " \ + "OR name LIKE '%.REAL.%' AND provider = ?" if date: sql += ' AND time >= ' + str(int(time.mktime(date.timetuple()))) @@ -292,7 +293,7 @@ class TVCache: for curResult in sqlResults: # skip non-tv crap - if not show_name_helpers.pass_wordlist_checks(curResult['name'], parse=False): + if not show_name_helpers.pass_wordlist_checks(curResult['name'], parse=False, indexer_lookup=False): continue # get the show object, or if it's not one of our shows then ignore it @@ -341,6 +342,18 @@ class TVCache: result.release_group = curReleaseGroup result.version = curVersion result.content = None + np = NameParser(False, showObj=showObj) + try: + parsed_result = np.parse(title) + extra_info_no_name = parsed_result.extra_info_no_name() + version = parsed_result.version + is_anime = parsed_result.is_anime + except (StandardError, Exception): + extra_info_no_name = None + version = -1 + is_anime = False + result.is_repack, result.properlevel = Quality.get_proper_level(extra_info_no_name, version, is_anime, + check_is_repack=True) # add it to the list if epObj not in neededEps: diff --git a/sickbeard/version_checker.py b/sickbeard/version_checker.py index 2be5aa4a..c1c8b3d9 100644 --- a/sickbeard/version_checker.py +++ b/sickbeard/version_checker.py @@ -410,8 +410,7 @@ class GitUpdateManager(UpdateManager): self._find_installed_version() # Notify update successful - if sickbeard.NOTIFY_ON_UPDATE: - notifiers.notify_git_update(sickbeard.CUR_COMMIT_HASH if sickbeard.CUR_COMMIT_HASH else "") + notifiers.notify_git_update(sickbeard.CUR_COMMIT_HASH if sickbeard.CUR_COMMIT_HASH else "") return True return False diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py index 3e5635d0..5444c91a 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/webapi.py @@ -36,7 +36,7 @@ from sickbeard import classes from sickbeard import processTV from sickbeard import network_timezones, sbdatetime from sickbeard.exceptions import ex -from sickbeard.common import SNATCHED, SNATCHED_PROPER, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED, UNKNOWN +from sickbeard.common import SNATCHED, SNATCHED_ANY, SNATCHED_PROPER, SNATCHED_BEST, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED, UNKNOWN from sickbeard.helpers import remove_article from common import Quality, qualityPresetStrings, statusStrings from sickbeard.indexers.indexer_config import * @@ -144,7 +144,7 @@ class Api(webserve.BaseHandler): out = '%s(%s);' % (callback, out) # wrap with JSONP call if requested except Exception as e: # if we fail to generate the output fake an error - logger.log(u'API :: ' + traceback.format_exc(), logger.DEBUG) + logger.log(u'API :: ' + traceback.format_exc(), logger.ERROR) out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex( e) + '"}' @@ -562,7 +562,7 @@ def _replace_statusStrings_with_statusCodes(statusStrings): if "wanted" in statusStrings: statusCodes.append(WANTED) if "archived" in statusStrings: - statusCodes.append(ARCHIVED) + statusCodes += Quality.ARCHIVED if "ignored" in statusStrings: statusCodes.append(IGNORED) if "unaired" in statusStrings: @@ -703,7 +703,7 @@ class CMD_ComingEpisodes(ApiCall): recently = (yesterday_dt - datetime.timedelta(days=sickbeard.EPISODE_VIEW_MISSED_RANGE)).toordinal() done_show_list = [] - qualList = Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED, IGNORED] + qualList = Quality.SNATCHED + Quality.DOWNLOADED + Quality.ARCHIVED + [IGNORED] myDB = db.DBConnection() sql_results = myDB.select( @@ -2095,7 +2095,7 @@ class CMD_ShowAddNew(ApiCall): sickbeard.showQueueScheduler.action.addShow(int(self.indexer), int(self.indexerid), showPath, newStatus, newQuality, int(self.flatten_folders), self.lang, self.subtitles, self.anime, - self.scene) # @UndefinedVariable + self.scene, new_show=True) # @UndefinedVariable return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added") @@ -2469,7 +2469,7 @@ class CMD_ShowStats(ApiCall): episode_status_counts_total = {} episode_status_counts_total["total"] = 0 for status in statusStrings.statusStrings.keys(): - if status in [UNKNOWN, DOWNLOADED, SNATCHED, SNATCHED_PROPER]: + if status in SNATCHED_ANY + [UNKNOWN, DOWNLOADED]: continue episode_status_counts_total[status] = 0 @@ -2485,7 +2485,7 @@ class CMD_ShowStats(ApiCall): # add all snatched qualities episode_qualities_counts_snatch = {} episode_qualities_counts_snatch["total"] = 0 - for statusCode in Quality.SNATCHED + Quality.SNATCHED_PROPER: + for statusCode in Quality.SNATCHED_ANY: status, quality = Quality.splitCompositeStatus(statusCode) if quality in [Quality.NONE]: continue @@ -2503,7 +2503,7 @@ class CMD_ShowStats(ApiCall): if status in Quality.DOWNLOADED: episode_qualities_counts_download["total"] += 1 episode_qualities_counts_download[int(row["status"])] += 1 - elif status in Quality.SNATCHED + Quality.SNATCHED_PROPER: + elif status in Quality.SNATCHED_ANY: episode_qualities_counts_snatch["total"] += 1 episode_qualities_counts_snatch[int(row["status"])] += 1 elif status == 0: # we dont count NONE = 0 = N/A @@ -2655,12 +2655,12 @@ class CMD_ShowsStats(ApiCall): [show for show in sickbeard.showList if show.paused == 0 and show.status != "Ended"]) stats["ep_downloaded"] = myDB.select("SELECT COUNT(*) FROM tv_episodes WHERE status IN (" + ",".join( [str(show) for show in - Quality.DOWNLOADED + [ARCHIVED]]) + ") AND season != 0 and episode != 0 AND airdate <= " + today + "")[0][ + Quality.DOWNLOADED + Quality.ARCHIVED]) + ") AND season != 0 and episode != 0 AND airdate <= " + today + "")[0][ 0] stats["ep_total"] = myDB.select( "SELECT COUNT(*) FROM tv_episodes WHERE season != 0 AND episode != 0 AND (airdate != 1 OR status IN (" + ",".join( - [str(show) for show in (Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER) + [ - ARCHIVED]]) + ")) AND airdate <= " + today + " AND status != " + str(IGNORED) + "")[0][0] + [str(show) for show in Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED]) + + ")) AND airdate <= " + today + " AND status != " + str(IGNORED) + "")[0][0] return _responds(RESULT_SUCCESS, stats) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c7a46951..7e1c45b8 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -44,11 +44,11 @@ from sickbeard import config, sab, nzbget, clients, history, notifiers, processT from sickbeard import encodingKludge as ek from sickbeard.providers import newznab, rsstorrent from sickbeard.common import Quality, Overview, statusStrings, qualityPresetStrings -from sickbeard.common import SNATCHED, UNAIRED, IGNORED, ARCHIVED, WANTED, FAILED, SKIPPED, DOWNLOADED, SNATCHED_BEST, SNATCHED_PROPER +from sickbeard.common import SNATCHED, SNATCHED_ANY, UNAIRED, IGNORED, ARCHIVED, WANTED, FAILED, SKIPPED, DOWNLOADED from sickbeard.common import SD, HD720p, HD1080p, UHD2160p -from sickbeard.exceptions import ex +from sickbeard.exceptions import ex, MultipleShowObjectsException from sickbeard.helpers import has_image_ext, remove_article, starify -from sickbeard.indexers.indexer_config import INDEXER_TVDB, INDEXER_TVRAGE +from sickbeard.indexers.indexer_config import INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_TRAKT from sickbeard.scene_numbering import get_scene_numbering, set_scene_numbering, get_scene_numbering_for_show, \ get_xem_numbering_for_show, get_scene_absolute_numbering_for_show, get_xem_absolute_numbering_for_show, \ get_scene_absolute_numbering @@ -57,18 +57,23 @@ from sickbeard.browser import foldersAtPath from sickbeard.blackandwhitelist import BlackAndWhiteList, short_group_names from sickbeard.search_backlog import FORCED_BACKLOG from sickbeard.indexermapper import MapStatus, save_mapping, map_indexers_to_show +from sickbeard.tv import show_not_found_retry_days, concurrent_show_not_found_days from tornado import gen from tornado.web import RequestHandler, StaticFileHandler, authenticated from lib import adba from lib import subliminal from lib.dateutil import tz import lib.rarfile.rarfile as rarfile +from unidecode import unidecode from lib.libtrakt import TraktAPI from lib.libtrakt.exceptions import TraktException, TraktAuthException +from lib.libtrakt.indexerapiinterface import TraktSearchTypes from trakt_helpers import build_config, trakt_collection_remove_account from sickbeard.bs4_parser import BS4Parser from lib.tmdb_api import TMDB +from lib.tvdb_api.tvdb_exceptions import tvdb_exception +from lib.fuzzywuzzy import fuzz try: import json @@ -91,6 +96,8 @@ class PageTemplate(Template): self.sbThemeName = sickbeard.THEME_NAME self.log_num_errors = len(classes.ErrorViewer.errors) + self.log_num_not_found_shows = len([x for x in sickbeard.showList if 0 < x.not_found_count]) + self.log_num_not_found_shows_all = len([x for x in sickbeard.showList if 0 != x.not_found_count]) self.sbPID = str(sickbeard.PID) self.menu = [ {'title': 'Home', 'key': 'home'}, @@ -222,7 +229,7 @@ class CalendarHandler(BaseHandler): # Limit dates past_date = (datetime.date.today() + datetime.timedelta(weeks=-52)).toordinal() future_date = (datetime.date.today() + datetime.timedelta(weeks=52)).toordinal() - utc = tz.gettz('GMT') + utc = tz.gettz('GMT', zoneinfo_priority=True) # Get all the shows that are not paused and are currently on air myDB = db.DBConnection() @@ -445,7 +452,7 @@ class MainHandler(WebHandler): recently = (yesterday_dt - datetime.timedelta(days=sickbeard.EPISODE_VIEW_MISSED_RANGE)).toordinal() done_show_list = [] - qualities = Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED, IGNORED] + qualities = Quality.SNATCHED + Quality.DOWNLOADED + Quality.ARCHIVED + [IGNORED, SKIPPED] myDB = db.DBConnection() sql_results = myDB.select( @@ -460,8 +467,8 @@ class MainHandler(WebHandler): 'SELECT *, tv_shows.status as show_status FROM tv_episodes outer_eps, tv_shows WHERE season != 0 AND showid NOT IN (%s)' % ','.join(['?'] * len(done_show_list)) + ' AND tv_shows.indexer_id = outer_eps.showid AND airdate = (SELECT airdate FROM tv_episodes inner_eps WHERE inner_eps.season != 0 AND inner_eps.showid = outer_eps.showid AND inner_eps.airdate >= ? ORDER BY inner_eps.airdate ASC LIMIT 1) AND outer_eps.status NOT IN (%s)' - % ','.join(['?'] * len(Quality.DOWNLOADED + Quality.SNATCHED)), - done_show_list + [next_week] + Quality.DOWNLOADED + Quality.SNATCHED) + % ','.join(['?'] * len(Quality.SNATCHED + Quality.DOWNLOADED)), + done_show_list + [next_week] + Quality.SNATCHED + Quality.DOWNLOADED) sql_results += myDB.select( 'SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND tv_shows.indexer_id = tv_episodes.showid AND airdate <= ? AND airdate >= ? AND tv_episodes.status = ? AND tv_episodes.status NOT IN (%s)' @@ -473,7 +480,7 @@ class MainHandler(WebHandler): # make a dict out of the sql results sql_results = [dict(row) for row in sql_results if Quality.splitCompositeStatus(helpers.tryInt(row['status']))[0] not in - [DOWNLOADED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, ARCHIVED, IGNORED]] + SNATCHED_ANY + [DOWNLOADED, ARCHIVED, IGNORED, SKIPPED]] # multi dimension sort sorts = { @@ -618,10 +625,10 @@ class Home(MainHandler): def HomeMenu(self): return [ {'title': 'Process Media', 'path': 'home/postprocess/'}, - {'title': 'Update Emby', 'path': 'home/updateEMBY/', 'requires': self.haveEMBY}, - {'title': 'Update Kodi', 'path': 'home/updateKODI/', 'requires': self.haveKODI}, - {'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': self.haveXBMC}, - {'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': self.havePLEX} + {'title': 'Update Emby', 'path': 'home/update_emby/', 'requires': self.haveEMBY}, + {'title': 'Update Kodi', 'path': 'home/update_kodi/', 'requires': self.haveKODI}, + {'title': 'Update XBMC', 'path': 'home/update_xbmc/', 'requires': self.haveXBMC}, + {'title': 'Update Plex', 'path': 'home/update_plex/', 'requires': self.havePLEX} ] @staticmethod @@ -720,8 +727,8 @@ class Home(MainHandler): # Get all show snatched / downloaded / next air date stats myDB = db.DBConnection() today = datetime.date.today().toordinal() - status_quality = ','.join([str(x) for x in Quality.SNATCHED + Quality.SNATCHED_PROPER]) - status_download = ','.join([str(x) for x in Quality.DOWNLOADED + [ARCHIVED]]) + status_quality = ','.join([str(x) for x in Quality.SNATCHED_ANY]) + status_download = ','.join([str(x) for x in Quality.DOWNLOADED + Quality.ARCHIVED]) status_total = '%s, %s, %s' % (SKIPPED, WANTED, FAILED) sql_statement = 'SELECT showid, ' @@ -776,236 +783,191 @@ class Home(MainHandler): client = clients.get_client_instance(torrent_method) - connection, accesMsg = client(host, username, password).test_authentication() + connection, acces_msg = client(host, username, password).test_authentication() - return accesMsg + return acces_msg - def testGrowl(self, host=None, password=None): + @staticmethod + def discover_emby(): + return notifiers.NotifierFactory().get('EMBY').discover_server() + + def test_emby(self, host=None, apikey=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + hosts = config.clean_hosts(host, default_port=8096) + if not hosts: + return 'Fail: No valid host(s)' + + result = notifiers.NotifierFactory().get('EMBY').test_notify(hosts, apikey) + + ui.notifications.message('Tested Emby:', urllib.unquote_plus(hosts.replace(',', ', '))) + return result + + def test_kodi(self, host=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + hosts = config.clean_hosts(host, default_port=8080) + if not hosts: + return 'Fail: No valid host(s)' + + if None is not password and set('*') == set(password): + password = sickbeard.KODI_PASSWORD + + result = notifiers.NotifierFactory().get('KODI').test_notify(hosts, username, password) + + ui.notifications.message('Tested Kodi:', urllib.unquote_plus(hosts.replace(',', ', '))) + return result + + def test_plex(self, host=None, username=None, password=None, server=False): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + hosts = config.clean_hosts(host, default_port=32400) + if not hosts: + return 'Fail: No valid host(s)' + + if None is not password and set('*') == set(password): + password = sickbeard.PLEX_PASSWORD + + server = 'true' == server + n = notifiers.NotifierFactory().get('PLEX') + method = n.test_update_library if server else n.test_notify + result = method(hosts, username, password) + + ui.notifications.message('Tested Plex %s(s): ' % ('client', 'Media Server host')[server], + urllib.unquote_plus(hosts.replace(',', ', '))) + return result + + # def test_xbmc(self, host=None, username=None, password=None): + # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + # + # hosts = config.clean_hosts(host, default_port=80) + # if not hosts: + # return 'Fail: No valid host(s)' + # + # if None is not password and set('*') == set(password): + # password = sickbeard.XBMC_PASSWORD + # + # result = notifiers.NotifierFactory().get('XBMC').test_notify(hosts, username, password) + # + # ui.notifications.message('Tested XBMC: ', urllib.unquote_plus(hosts.replace(',', ', '))) + # return result + + def test_nmj(self, host=None, database=None, mount=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + if not hosts: + return 'Fail: No valid host(s)' + + return notifiers.NotifierFactory().get('NMJ').test_notify(urllib.unquote_plus(host), database, mount) + + def settings_nmj(self, host=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + if not hosts: + return 'Fail: No valid host(s)' + + return notifiers.NotifierFactory().get('NMJ').notify_settings(urllib.unquote_plus(host)) + + def test_nmj2(self, host=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + if not hosts: + return 'Fail: No valid host(s)' + + return notifiers.NotifierFactory().get('NMJV2').test_notify(urllib.unquote_plus(host)) + + def settings_nmj2(self, host=None, dbloc=None, instance=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + return notifiers.NotifierFactory().get('NMJV2').notify_settings(urllib.unquote_plus(host), dbloc, instance) + + def test_boxcar2(self, access_token=None, sound=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not access_token and starify(access_token, True): + access_token = sickbeard.BOXCAR2_ACCESSTOKEN + + return notifiers.NotifierFactory().get('BOXCAR2').test_notify(access_token, sound) + + def test_pushbullet(self, access_token=None, device_iden=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not access_token and starify(access_token, True): + access_token = sickbeard.PUSHBULLET_ACCESS_TOKEN + + return notifiers.NotifierFactory().get('PUSHBULLET').test_notify(access_token, device_iden) + + def get_pushbullet_devices(self, access_token=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not access_token and starify(access_token, True): + access_token = sickbeard.PUSHBULLET_ACCESS_TOKEN + + return notifiers.NotifierFactory().get('PUSHBULLET').get_devices(access_token) + + def test_pushover(self, user_key=None, api_key=None, priority=None, device=None, sound=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not user_key and starify(user_key, True): + user_key = sickbeard.PUSHOVER_USERKEY + + if None is not api_key and starify(api_key, True): + api_key = sickbeard.PUSHOVER_APIKEY + + return notifiers.NotifierFactory().get('PUSHOVER').test_notify(user_key, api_key, priority, device, sound) + + def get_pushover_devices(self, user_key=None, api_key=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not user_key and starify(user_key, True): + user_key = sickbeard.PUSHOVER_USERKEY + + if None is not api_key and starify(api_key, True): + api_key = sickbeard.PUSHOVER_APIKEY + + return notifiers.NotifierFactory().get('PUSHOVER').get_devices(user_key, api_key) + + def test_growl(self, host=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') host = config.clean_host(host, default_port=23053) if None is not password and set('*') == set(password): password = sickbeard.GROWL_PASSWORD - result = notifiers.growl_notifier.test_notify(host, password) - if password is None or password == '': - pw_append = '' - else: - pw_append = ' with password: ' + password + return notifiers.NotifierFactory().get('GROWL').test_notify(host, password) - if result: - return 'Registered and Tested growl successfully ' + urllib.unquote_plus(host) + pw_append - else: - return 'Registration and Testing of growl failed ' + urllib.unquote_plus(host) + pw_append - - def testProwl(self, prowl_api=None, prowl_priority=0): + def test_prowl(self, prowl_api=None, prowl_priority=0): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') if None is not prowl_api and starify(prowl_api, True): prowl_api = sickbeard.PROWL_API - result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) - if result: - return 'Test prowl notice sent successfully' - else: - return 'Test prowl notice failed' + return notifiers.NotifierFactory().get('PROWL').test_notify(prowl_api, prowl_priority) - def testBoxcar2(self, accesstoken=None, sound=None): + def test_nma(self, nma_api=None, nma_priority=0): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - if None is not accesstoken and starify(accesstoken, True): - accesstoken = sickbeard.BOXCAR2_ACCESSTOKEN + if None is not nma_api and starify(nma_api, True): + nma_api = sickbeard.NMA_API - result = notifiers.boxcar2_notifier.test_notify(accesstoken, sound) - if result: - return 'Boxcar2 notification succeeded. Check your Boxcar2 clients to make sure it worked' - else: - return 'Error sending Boxcar2 notification' + return notifiers.NotifierFactory().get('NMA').test_notify(nma_api, nma_priority) - def testPushover(self, userKey=None, apiKey=None, priority=None, device=None, sound=None): + def test_libnotify(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - if None is not userKey and starify(userKey, True): - userKey = sickbeard.PUSHOVER_USERKEY + return notifiers.NotifierFactory().get('LIBNOTIFY').test_notify() - if None is not apiKey and starify(apiKey, True): - apiKey = sickbeard.PUSHOVER_APIKEY - - result = notifiers.pushover_notifier.test_notify(userKey, apiKey, priority, device, sound) - if result: - return 'Pushover notification succeeded. Check your Pushover clients to make sure it worked' - else: - return 'Error sending Pushover notification' - - def getPushoverDevices(self, userKey=None, apiKey=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not userKey and starify(userKey, True): - userKey = sickbeard.PUSHOVER_USERKEY - - if None is not apiKey and starify(apiKey, True): - apiKey = sickbeard.PUSHOVER_APIKEY - - result = notifiers.pushover_notifier.get_devices(userKey, apiKey) - if result: - return result - else: - return "{}" - - def twitterStep1(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - return notifiers.twitter_notifier._get_authorization() - - def twitterStep2(self, key): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.twitter_notifier._get_credentials(key) - logger.log(u'result: ' + str(result)) - if result: - return 'Key verification successful' - else: - return 'Unable to verify key' - - def testTwitter(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.twitter_notifier.test_notify() - if result: - return 'Tweet successful, check your twitter to make sure it worked' - else: - return 'Error sending tweet' - - @staticmethod - def discover_emby(): - return notifiers.emby_notifier.discover_server() - - def testEMBY(self, host=None, apikey=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - hosts = config.clean_hosts(host) - if not hosts: - return 'Fail: At least one invalid host' - - total_success, cur_message = notifiers.emby_notifier.test_notify(hosts, apikey) - return (cur_message, u'Success. All Emby hosts tested.')[total_success] - - def testKODI(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - hosts = config.clean_hosts(host) - if not hosts: - return 'Fail: At least one invalid host' - - if None is not password and set('*') == set(password): - password = sickbeard.KODI_PASSWORD - - total_success, cur_message = notifiers.kodi_notifier.test_notify(hosts, username, password) - return (cur_message, u'Success. All Kodi hosts tested.')[total_success] - - def testXBMC(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_hosts(host) - if None is not password and set('*') == set(password): - password = sickbeard.XBMC_PASSWORD - - finalResult = '' - for curHost in [x.strip() for x in host.split(',')]: - curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: - finalResult += 'Test XBMC notice sent successfully to ' + urllib.unquote_plus(curHost) - else: - finalResult += 'Test XBMC notice failed to ' + urllib.unquote_plus(curHost) - finalResult += "
    \n" - - return finalResult - - def testPMC(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not password and set('*') == set(password): - password = sickbeard.PLEX_PASSWORD - - finalResult = '' - for curHost in [x.strip() for x in host.split(',')]: - curResult = notifiers.plex_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: - finalResult += 'Successful test notice sent to Plex client ... ' + urllib.unquote_plus(curHost) - else: - finalResult += 'Test failed for Plex client ... ' + urllib.unquote_plus(curHost) - finalResult += '
    ' + '\n' - - ui.notifications.message('Tested Plex client(s): ', urllib.unquote_plus(host.replace(',', ', '))) - - return finalResult - - def testPMS(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not password and set('*') == set(password): - password = sickbeard.PLEX_PASSWORD - - cur_result = notifiers.plex_notifier.test_notify(urllib.unquote_plus(host), username, password, server=True) - if '
    ' == cur_result: - cur_result += 'Fail: No valid host set to connect with' - final_result = (('Test result for', 'Successful test of')['Fail' not in cur_result] - + ' Plex server(s) ... %s
    \n' % cur_result) - - ui.notifications.message('Tested Plex Media Server host(s): ', urllib.unquote_plus(host.replace(',', ', '))) - - return final_result - - def testLibnotify(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if notifiers.libnotify_notifier.test_notify(): - return 'Tried sending desktop notification via libnotify' - else: - return notifiers.libnotify.diagnose() - - def testNMJ(self, host=None, database=None, mount=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) - if result: - return 'Successfully started the scan update' - else: - return 'Test failed to start the scan update' - - def settingsNMJ(self, host=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) - if result: - return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % { - "host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} - else: - return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' - - def testNMJv2(self, host=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmjv2_notifier.test_notify(urllib.unquote_plus(host)) - if result: - return 'Test notice sent successfully to ' + urllib.unquote_plus(host) - else: - return 'Test notice failed to ' + urllib.unquote_plus(host) - - def settingsNMJv2(self, host=None, dbloc=None, instance=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmjv2_notifier.notify_settings(urllib.unquote_plus(host), dbloc, instance) - if result: - return '{"message": "NMJ Database found at: %(host)s", "database": "%(database)s"}' % {"host": host, - "database": sickbeard.NMJv2_DATABASE} - else: - return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { - "dbloc": dbloc} + # def test_pushalot(self, authorization_token=None): + # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + # + # if None is not authorization_token and starify(authorization_token, True): + # authorization_token = sickbeard.PUSHALOT_AUTHORIZATIONTOKEN + # + # return notifiers.NotifierFactory().get('PUSHALOT').test_notify(authorization_token) def trakt_authenticate(self, pin=None, account=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -1056,7 +1018,7 @@ class Home(MainHandler): return json.dumps({'result': 'Not found: Account to delete'}) return json.dumps({'result': 'Not found: Invalid account id'}) - def loadShowNotifyLists(self, *args, **kwargs): + def load_show_notify_lists(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') my_db = db.DBConnection() @@ -1079,6 +1041,51 @@ class Home(MainHandler): return json.dumps(response) + def test_slack(self, channel=None, as_authed=False, bot_name=None, icon_url=None, access_token=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.NotifierFactory().get('SLACK').test_notify( + channel=channel, as_authed='true' == as_authed, + bot_name=bot_name, icon_url=icon_url, access_token=access_token) + + def test_discordapp(self, as_authed=False, username=None, icon_url=None, as_tts=False, access_token=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.NotifierFactory().get('DISCORDAPP').test_notify( + as_authed='true' == as_authed, username=username, icon_url=icon_url, + as_tts='true' == as_tts, access_token=access_token) + + def test_gitter(self, room_name=None, access_token=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.NotifierFactory().get('GITTER').test_notify( + room_name=room_name, access_token=access_token) + + def test_twitter(self, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.NotifierFactory().get('TWITTER').test_notify() + + def twitter_step1(self, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.NotifierFactory().get('TWITTER').get_authorization() + + def twitter_step2(self, key): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.NotifierFactory().get('TWITTER').get_credentials(key) + + def test_email(self, host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if None is not pwd and set('*') == set(pwd): + pwd = sickbeard.EMAIL_PASSWORD + + host = config.clean_host(host) + + return notifiers.NotifierFactory().get('EMAIL').test_notify(host, port, smtp_from, use_tls, user, pwd, to) + @staticmethod def save_show_email(show=None, emails=None): # self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -1092,57 +1099,6 @@ class Home(MainHandler): success = True return json.dumps({'id': show, 'success': success}) - def testEmail(self, host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not pwd and set('*') == set(pwd): - pwd = sickbeard.EMAIL_PASSWORD - host = config.clean_host(host) - - if notifiers.email_notifier.test_notify(host, port, smtp_from, use_tls, user, pwd, to): - return 'Success. Test email sent. Check inbox.' - return 'ERROR: %s' % notifiers.email_notifier.last_err - - def testNMA(self, nma_api=None, nma_priority=0): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not nma_api and starify(nma_api, True): - nma_api = sickbeard.NMA_API - - result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) - if result: - return 'Test NMA notice sent successfully' - else: - return 'Test NMA notice failed' - - def testPushalot(self, authorizationToken=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not authorizationToken and starify(authorizationToken, True): - authorizationToken = sickbeard.PUSHALOT_AUTHORIZATIONTOKEN - - result = notifiers.pushalot_notifier.test_notify(authorizationToken) - if result: - return 'Pushalot notification succeeded. Check your Pushalot clients to make sure it worked' - else: - return 'Error sending Pushalot notification' - - def testPushbullet(self, accessToken=None, device_iden=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not accessToken and starify(accessToken, True): - accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN - - return notifiers.pushbullet_notifier.test_notify(accessToken, device_iden) - - def getPushbulletDevices(self, accessToken=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if None is not accessToken and starify(accessToken, True): - accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN - - return notifiers.pushbullet_notifier.get_devices(accessToken) - def viewchanges(self): t = PageTemplate(headers=self.request.headers, file='viewchanges.tmpl') @@ -1314,6 +1270,14 @@ class Home(MainHandler): elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(showObj): # @UndefinedVariable show_message = 'This show is queued and awaiting subtitles download.' + if 0 != showObj.not_found_count: + last_found = ('', ' since %s' % sbdatetime.sbdatetime.fromordinal( + showObj.last_found_on_indexer).sbfdate())[1 < showObj.last_found_on_indexer] + show_message = ( + 'The master ID of this show has been abandoned%s, ' % last_found + + 'replace it here' % ( + sickbeard.WEB_ROOT, show, show) + + ('', '
    %s' % show_message)[0 < len(show_message)]) t.force_update = 'home/updateShow?show=%d&force=1&web=1' % showObj.indexerid if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): # @UndefinedVariable if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable @@ -1323,14 +1287,14 @@ class Home(MainHandler): t.submenu.append( {'title': 'Force Full Update', 'path': t.force_update}) t.submenu.append({'title': 'Update show in Emby', - 'path': 'home/updateEMBY%s' % + 'path': 'home/update_emby%s' % (INDEXER_TVDB == showObj.indexer and ('?show=%s' % showObj.indexerid) or '/'), 'requires': self.haveEMBY}) t.submenu.append({'title': 'Update show in Kodi', - 'path': 'home/updateKODI?showName=%s' % urllib.quote_plus( + 'path': 'home/update_kodi?show_name=%s' % urllib.quote_plus( showObj.name.encode('utf-8')), 'requires': self.haveKODI}) t.submenu.append({'title': 'Update show in XBMC', - 'path': 'home/updateXBMC?showName=%s' % urllib.quote_plus( + 'path': 'home/update_xbmc?show_name=%s' % urllib.quote_plus( showObj.name.encode('utf-8')), 'requires': self.haveXBMC}) t.submenu.append({'title': 'Media Renamer', 'path': 'home/testRename?show=%d' % showObj.indexerid}) if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( @@ -1339,6 +1303,13 @@ class Home(MainHandler): {'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d' % showObj.indexerid}) t.show = showObj + with BS4Parser('%s' % showObj.overview, features=['html5lib', 'permissive']) as soup: + try: + soup.a.replace_with(soup.new_tag('')) + except(StandardError, Exception): + pass + overview = re.sub('(?i)full streaming', '', soup.get_text().strip()) + t.show.overview = overview t.show_message = show_message ep_counts = {} @@ -1380,7 +1351,7 @@ class Home(MainHandler): for row in my_db.select('SELECT max(season) as latest FROM tv_episodes WHERE showid = ?' + ' and 1000 < airdate and ? < status', [showObj.indexerid, UNAIRED]): - t.latest_season = row['latest'] + t.latest_season = row['latest'] or {0: 1, 1: 1, 2: None}.get(sickbeard.DISPLAY_SHOW_VIEWMODE) t.season_min = ([], [1])[2 < t.latest_season] + [t.latest_season] t.other_seasons = (list(set(all_seasons) - set(t.season_min)), [])[display_show_minimum] @@ -1403,10 +1374,13 @@ class Home(MainHandler): status_overview = showObj.getOverview(row['status']) if status_overview: ep_counts[status_overview] += row['cnt'] - if ARCHIVED == row['status']: - ep_counts['archived'].setdefault(row['season'], row['cnt']) + if ARCHIVED == Quality.splitCompositeStatus(row['status'])[0]: + ep_counts['archived'].setdefault(row['season'], 0) + ep_counts['archived'][row['season']] = row['cnt'] + ep_counts['archived'].get(row['season'], 0) else: - ep_counts['status'].setdefault(row['season'], {status_overview: row['cnt']}) + ep_counts['status'].setdefault(row['season'], {}) + ep_counts['status'][row['season']][status_overview] = row['cnt'] + \ + ep_counts['status'][row['season']].get(status_overview, 0) for row in my_db.select('SELECT season, count(*) AS cnt FROM tv_episodes WHERE showid = ?' + ' AND \'\' != location GROUP BY season', [showObj.indexerid]): @@ -1468,6 +1442,7 @@ class Home(MainHandler): indexerid = int(showObj.indexerid) indexer = int(showObj.indexer) + t.min_initial = Quality.qualityStrings[min(Quality.splitQuality(showObj.quality)[0])] t.all_scene_exceptions = showObj.exceptions t.scene_numbering = get_scene_numbering_for_show(indexerid, indexer) t.scene_absolute_numbering = get_scene_absolute_numbering_for_show(indexerid, indexer) @@ -1581,7 +1556,8 @@ class Home(MainHandler): return {'Success': 'Switched to new TV info source'} def saveMapping(self, show, **kwargs): - show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + show = helpers.tryInt(show) + show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, show) response = {} if not show_obj: return json.dumps(response) @@ -1621,10 +1597,22 @@ class Home(MainHandler): else: ui.notifications.message('Mappings unchanged, not saving.') - master_ids = [show] + [kwargs.get(x) for x in 'indexer', 'mindexerid', 'mindexer'] - if all([helpers.tryInt(x) > 0 for x in master_ids]): - master_ids += [bool(helpers.tryInt(kwargs.get(x))) for x in 'paused', 'markwanted'] - response = {'switch': self.switchIndexer(*master_ids), 'mid': kwargs['mindexerid']} + master_ids = [show] + [helpers.tryInt(kwargs.get(x)) for x in 'indexer', 'mindexerid', 'mindexer'] + if all([x > 0 for x in master_ids]) and sickbeard.indexerApi(kwargs['mindexer']).config.get('active') and \ + not sickbeard.indexerApi(kwargs['mindexer']).config.get('defunct') and \ + not sickbeard.indexerApi(kwargs['mindexer']).config.get('mapped_only') and \ + (helpers.tryInt(kwargs['mindexer']) != helpers.tryInt(kwargs['indexer']) or + helpers.tryInt(kwargs['mindexerid']) != show): + try: + new_show_obj = helpers.find_show_by_id(sickbeard.showList, {helpers.tryInt(kwargs['mindexer']): helpers.tryInt(kwargs['mindexerid'])},no_mapped_ids=False) + if not new_show_obj or (new_show_obj.indexer == show_obj.indexer and new_show_obj.indexerid == show_obj.indexerid): + master_ids += [bool(helpers.tryInt(kwargs.get(x))) for x in 'paused', 'markwanted'] + response = {'switch': self.switchIndexer(*master_ids), 'mid': kwargs['mindexerid']} + else: + ui.notifications.message('Master ID unchanged, because show from %s with ID: %s exists in DB.' % + (sickbeard.indexerApi(kwargs['mindexer']).name, kwargs['mindexerid'])) + except MultipleShowObjectsException: + pass response.update({ 'map': {k: {r: w for r, w in v.iteritems() if r != 'date'} for k, v in show_obj.ids.iteritems()} @@ -1719,6 +1707,10 @@ class Home(MainHandler): t = PageTemplate(headers=self.request.headers, file='editShow.tmpl') t.submenu = self.HomeMenu() + t.expand_ids = all([kwargs.get('tvsrc'), kwargs.get('srcid')]) + t.tvsrc = int(kwargs.get('tvsrc', 0)) + t.srcid = kwargs.get('srcid') + myDB = db.DBConnection() t.seasonResults = myDB.select( 'SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season asc', [showObj.indexerid]) @@ -1747,6 +1739,20 @@ class Home(MainHandler): self.fanart_tmpl(t) t.num_ratings = len(sickbeard.FANART_RATINGS.get(str(t.show.indexerid), {})) + t.unlock_master_id = 0 != showObj.not_found_count + t.showname_enc = urllib.quote_plus(showObj.name.encode('utf-8')) + + show_message = '' + + if 0 != showObj.not_found_count: + # noinspection PyUnresolvedReferences + last_found = ('', ' since %s' % sbdatetime.sbdatetime.fromordinal( + showObj.last_found_on_indexer).sbfdate())[1 < showObj.last_found_on_indexer] + show_message = 'The master ID of this show has been abandoned%s
    search for ' % last_found + \ + 'a replacement in the "Related show IDs" section of the "Other" tab' + + t.show_message = show_message + return t.respond() flatten_folders = config.checkbox_to_value(flatten_folders) @@ -1979,16 +1985,16 @@ class Home(MainHandler): self.redirect('/home/displayShow?show=' + str(showObj.indexerid)) - def updateEMBY(self, show=None): + def update_emby(self, show=None): - if notifiers.emby_notifier.update_library( - sickbeard.helpers.findCertainShow(sickbeard.showList,helpers.tryInt(show, None)), force=True): + if notifiers.NotifierFactory().get('EMBY').update_library( + sickbeard.helpers.findCertainShow(sickbeard.showList, helpers.tryInt(show, None))): ui.notifications.message('Library update command sent to Emby host(s): ' + sickbeard.EMBY_HOST) else: ui.notifications.error('Unable to contact one or more Emby host(s): ' + sickbeard.EMBY_HOST) self.redirect('/home/') - def updateKODI(self, showName=None): + def update_kodi(self, show_name=None): # only send update to first host in the list -- workaround for kodi sql backend users if sickbeard.KODI_UPDATE_ONLYFIRST: @@ -1997,29 +2003,14 @@ class Home(MainHandler): else: host = sickbeard.KODI_HOST - if notifiers.kodi_notifier.update_library(showName=showName, force=True): + if notifiers.NotifierFactory().get('KODI').update_library(show_name=show_name): ui.notifications.message('Library update command sent to Kodi host(s): ' + host) else: ui.notifications.error('Unable to contact one or more Kodi host(s): ' + host) self.redirect('/home/') - def updateXBMC(self, showName=None): - - # only send update to first host in the list -- workaround for xbmc sql backend users - if sickbeard.XBMC_UPDATE_ONLYFIRST: - # only send update to first host in the list -- workaround for xbmc sql backend users - host = sickbeard.XBMC_HOST.split(',')[0].strip() - else: - host = sickbeard.XBMC_HOST - - if notifiers.xbmc_notifier.update_library(showName=showName): - ui.notifications.message('Library update command sent to XBMC host(s): ' + host) - else: - ui.notifications.error('Unable to contact one or more XBMC host(s): ' + host) - self.redirect('/home/') - - def updatePLEX(self, *args, **kwargs): - result = notifiers.plex_notifier.update_library() + def update_plex(self, *args, **kwargs): + result = notifiers.NotifierFactory().get('PLEX').update_library() if 'Fail' not in result: ui.notifications.message( 'Library update command sent to', 'Plex Media Server host(s): ' + sickbeard.PLEX_SERVER_HOST.replace(',', ', ')) @@ -2027,34 +2018,53 @@ class Home(MainHandler): ui.notifications.error('Unable to contact', 'Plex Media Server host(s): ' + result) self.redirect('/home/') + # def update_xbmc(self, show_name=None): + # + # # only send update to first host in the list -- workaround for xbmc sql backend users + # if sickbeard.XBMC_UPDATE_ONLYFIRST: + # # only send update to first host in the list -- workaround for xbmc sql backend users + # host = sickbeard.XBMC_HOST.split(',')[0].strip() + # else: + # host = sickbeard.XBMC_HOST + # + # if notifiers.NotifierFactory().get('XBMC').update_library(show_name=show_name): + # ui.notifications.message('Library update command sent to XBMC host(s): ' + host) + # else: + # ui.notifications.error('Unable to contact one or more XBMC host(s): ' + host) + # self.redirect('/home/') + def setStatus(self, show=None, eps=None, status=None, direct=False): if show is None or eps is None or status is None: - errMsg = 'You must specify a show and at least one episode' + err_msg = 'You must specify a show and at least one episode' if direct: - ui.notifications.error('Error', errMsg) + ui.notifications.error('Error', err_msg) return json.dumps({'result': 'error'}) - else: - return self._genericMessage('Error', errMsg) + return self._genericMessage('Error', err_msg) - if not statusStrings.has_key(int(status)): - errMsg = 'Invalid status' + use_default = False + if '-' in status: + use_default = True + status = status.replace('-', '') + status = int(status) + + if not statusStrings.has_key(status): + err_msg = 'Invalid status' if direct: - ui.notifications.error('Error', errMsg) + ui.notifications.error('Error', err_msg) return json.dumps({'result': 'error'}) - else: - return self._genericMessage('Error', errMsg) + return self._genericMessage('Error', err_msg) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if showObj is None: - errMsg = 'Error', 'Show not in show list' + err_msg = 'Error', 'Show not in show list' if direct: - ui.notifications.error('Error', errMsg) + ui.notifications.error('Error', err_msg) return json.dumps({'result': 'error'}) - else: - return self._genericMessage('Error', errMsg) + return self._genericMessage('Error', err_msg) + min_initial = min(Quality.splitQuality(showObj.quality)[0]) segments = {} if eps is not None: @@ -2063,53 +2073,58 @@ class Home(MainHandler): logger.log(u'Attempting to set status on episode %s to %s' % (curEp, status), logger.DEBUG) - epInfo = curEp.split('x') + ep_obj = showObj.getEpisode(*tuple([int(x) for x in curEp.split('x')])) - epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) + if ep_obj is None: + return self._genericMessage('Error', 'Episode couldn\'t be retrieved') - if epObj is None: - return self._genericMessage("Error", "Episode couldn't be retrieved") - - if int(status) in [WANTED, FAILED]: + if status in [WANTED, FAILED]: # figure out what episodes are wanted so we can backlog them - if epObj.season in segments: - segments[epObj.season].append(epObj) + if ep_obj.season in segments: + segments[ep_obj.season].append(ep_obj) else: - segments[epObj.season] = [epObj] + segments[ep_obj.season] = [ep_obj] - with epObj.lock: + with ep_obj.lock: + required = Quality.SNATCHED_ANY + Quality.DOWNLOADED + err_msg = '' # don't let them mess up UNAIRED episodes - if epObj.status == UNAIRED: - logger.log(u'Refusing to change status of ' + curEp + ' because it is UNAIRED', logger.ERROR) + if UNAIRED == ep_obj.status: + err_msg = 'because it is unaired' + + elif FAILED == status and ep_obj.status not in required: + err_msg = 'to failed because it\'s not snatched/downloaded' + + elif status in Quality.DOWNLOADED\ + and ep_obj.status not in required + Quality.ARCHIVED + [IGNORED, SKIPPED]\ + and not ek.ek(os.path.isfile, ep_obj.location): + err_msg = 'to downloaded because it\'s not snatched/downloaded/archived' + + if err_msg: + logger.log('Refusing to change status of %s %s' % (curEp, err_msg), logger.ERROR) continue - if int( - status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + Quality.DOWNLOADED + [ - IGNORED, SKIPPED] and not ek.ek(os.path.isfile, epObj.location): - logger.log( - u'Refusing to change status of ' + curEp + " to DOWNLOADED because it's not SNATCHED/DOWNLOADED", - logger.ERROR) - continue - - if int( - status) == FAILED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST + Quality.DOWNLOADED: - logger.log( - u'Refusing to change status of ' + curEp + " to FAILED because it's not SNATCHED/DOWNLOADED", - logger.ERROR) - continue - - epObj.status = int(status) + if ARCHIVED == status: + if ep_obj.status in Quality.DOWNLOADED: + ep_obj.status = Quality.compositeStatus( + ARCHIVED, (Quality.splitCompositeStatus(ep_obj.status)[1], min_initial)[use_default]) + elif DOWNLOADED == status: + if ep_obj.status in Quality.ARCHIVED: + ep_obj.status = Quality.compositeStatus( + DOWNLOADED, Quality.splitCompositeStatus(ep_obj.status)[1]) + else: + ep_obj.status = status # mass add to database - result = epObj.get_sql() + result = ep_obj.get_sql() if None is not result: sql_l.append(result) if 0 < len(sql_l): - myDB = db.DBConnection() - myDB.mass_action(sql_l) + my_db = db.DBConnection() + my_db.mass_action(sql_l) - if WANTED == int(status): + if WANTED == status: season_list = '' season_wanted = [] for season, segment in segments.items(): @@ -2132,27 +2147,25 @@ class Home(MainHandler): u'%s for the following seasons of %s:
      %s
    ' % (msg, showObj.name, season_list)) - elif FAILED == int(status): - msg = 'Retrying Search was automatically started for the following season of ' + showObj.name + ':
    ' - msg += '
      ' + elif FAILED == status: + msg = u'Retrying search automatically for the following season of %s:
        ' % showObj.name for season, segment in segments.items(): cur_failed_queue_item = search_queue.FailedQueueItem(showObj, segment) - sickbeard.searchQueueScheduler.action.add_item(cur_failed_queue_item) # @UndefinedVariable + sickbeard.searchQueueScheduler.action.add_item(cur_failed_queue_item) - msg += '
      • Season ' + str(season) + '
      • ' - logger.log(u'Retrying Search for ' + showObj.name + ' season ' + str( - season) + ' because some eps were set to failed') + msg += '
      • Season %s
      • ' % season + logger.log(u'Retrying search for %s season %s because some eps were set to failed' % + (showObj.name, season)) msg += '
      ' if segments: - ui.notifications.message('Retry Search started', msg) + ui.notifications.message('Retry search started', msg) if direct: return json.dumps({'result': 'success'}) - else: - self.redirect('/home/displayShow?show=' + show) + self.redirect('/home/displayShow?show=' + show) def testRename(self, show=None): @@ -2311,6 +2324,7 @@ class Home(MainHandler): 'status' : statusStrings[epObj.status], 'quality': self.getQualityClass(epObj)}) + retry_statues = SNATCHED_ANY + [DOWNLOADED, ARCHIVED] if currentManualSearchThreadActive: searchThread = currentManualSearchThreadActive searchstatus = 'searching' @@ -2323,6 +2337,7 @@ class Home(MainHandler): 'episodeindexid': searchThread.segment.indexerid, 'season' : searchThread.segment.season, 'searchstatus' : searchstatus, + 'retrystatus': Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, 'status' : statusStrings[searchThread.segment.status], 'quality': self.getQualityClass(searchThread.segment)}) elif hasattr(searchThread, 'segment'): @@ -2331,6 +2346,7 @@ class Home(MainHandler): 'episodeindexid': epObj.indexerid, 'season' : epObj.season, 'searchstatus' : searchstatus, + 'retrystatus': Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, 'status' : statusStrings[epObj.status], 'quality': self.getQualityClass(epObj)}) @@ -2343,6 +2359,7 @@ class Home(MainHandler): 'episodeindexid': searchThread.segment.indexerid, 'season' : searchThread.segment.season, 'searchstatus' : searchstatus, + 'retrystatus': Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, 'status' : statusStrings[searchThread.segment.status], 'quality': self.getQualityClass(searchThread.segment)}) ### These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segement/episodes @@ -2354,6 +2371,7 @@ class Home(MainHandler): 'episodeindexid': epObj.indexerid, 'season' : epObj.season, 'searchstatus' : searchstatus, + 'retrystatus': Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, 'status' : statusStrings[epObj.status], 'quality': self.getQualityClass(epObj)}) @@ -2493,7 +2511,7 @@ class HomePostProcess(Home): return t.respond() def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None, process_method=None, force=None, - force_replace=None, failed='0', type='auto', stream='0', dupekey=None, **kwargs): + force_replace=None, failed='0', type='auto', stream='0', dupekey=None, is_basedir='1', **kwargs): if not dir and ('0' == failed or not nzbName): self.redirect('/home/postprocess/') @@ -2517,7 +2535,7 @@ class HomePostProcess(Home): force_replace=force_replace in ['on', '1'], failed='0' != failed, webhandler=self.send_message if stream != '0' else None, - showObj=showObj) + showObj=showObj, is_basedir=is_basedir in ['on', '1']) if '0' != stream: return @@ -2549,31 +2567,47 @@ class NewHomeAddShows(Home): def sanitizeFileName(self, name): return helpers.sanitizeFileName(name) + # noinspection PyPep8Naming def searchIndexersForShowName(self, search_term, lang='en', indexer=None): - if not lang or lang == 'null': + if not lang or 'null' == lang: lang = 'en' - - search_term = search_term.strip().encode('utf-8') + term = search_term.decode('utf-8').strip() + terms = [] + try: + for t in term.encode('utf-8'), unidecode(term), term: + if t not in terms: + terms += [t] + except (StandardError, Exception): + terms = [search_term.strip().encode('utf-8')] results = {} final_results = [] - search_id, indexer_id = '', None - search_id = '' + search_id, indexer_id, trakt_id, tmdb_id, INDEXER_TVDB_X = '', None, None, None, INDEXER_TRAKT try: search_id = re.search(r'(?m)((?:tt\d{4,})|^\d{4,}$)', search_term).group(1) - resp = [r for r in self.getTrakt('/search/%s/%s?type=show&extended=full' % ( - ('tvdb', 'imdb')['tt' in search_id], search_id)) if 'show' == r['type']][0] - search_term = resp['show']['title'] - indexer_id = resp['show']['ids']['tvdb'] - except: + + lINDEXER_API_PARMS = sickbeard.indexerApi(INDEXER_TVDB_X).api_params.copy() + lINDEXER_API_PARMS['language'] = lang + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI + lINDEXER_API_PARMS['sleep_retry'] = 5 + lINDEXER_API_PARMS['search_type'] = (TraktSearchTypes.tvdb_id, TraktSearchTypes.imdb_id)['tt' in search_id] + t = sickbeard.indexerApi(INDEXER_TVDB_X).indexer(**lINDEXER_API_PARMS) + + resp = t[search_id][0] + search_term = resp['seriesname'] + indexer_id = resp['ids']['tvdb'] + trakt_id = resp['ids'].get('trakt') + tmdb_id = resp['ids'].get('tmdb') + + except (StandardError, Exception): search_term = (search_term, '')['tt' in search_id] - # Query Indexers for each search term and build the list of results + # query Indexers for search term and build list of results for indexer in sickbeard.indexerApi().indexers if not int(indexer) else [int(indexer)]: lINDEXER_API_PARMS = sickbeard.indexerApi(indexer).api_params.copy() lINDEXER_API_PARMS['language'] = lang - lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsListUI + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI t = sickbeard.indexerApi(indexer).indexer(**lINDEXER_API_PARMS) try: @@ -2581,78 +2615,152 @@ class NewHomeAddShows(Home): if bool(indexer_id): logger.log('Fetching show using id: %s (%s) from tv datasource %s' % ( search_id, search_term, sickbeard.indexerApi(indexer).name), logger.DEBUG) - results.setdefault('tt' in search_id and 3 or indexer, []).extend( - [{'id': indexer_id, 'seriesname': t[indexer_id, False]['seriesname'], - 'firstaired': t[indexer_id, False]['firstaired'], 'network': t[indexer_id, False]['network'], - 'overview': t[indexer_id, False]['overview'], - 'genres': '' if not t[indexer_id, False]['genre'] else - t[indexer_id, False]['genre'].lower().strip('|').replace('|', ', '), - }]) + r = t[indexer_id, False] + results.setdefault((indexer, INDEXER_TVDB_X)['tt' in search_id], {})[int(indexer_id)] = { + 'id': indexer_id, 'seriesname': r['seriesname'], 'firstaired': r['firstaired'], + 'network': r['network'], 'overview': r['overview'], + 'genres': '' if not r['genre'] else r['genre'].lower().strip('|').replace('|', ', '), + 'trakt_id': trakt_id, 'tmdb_id': tmdb_id + } break else: logger.log('Searching for shows using search term: %s from tv datasource %s' % ( search_term, sickbeard.indexerApi(indexer).name), logger.DEBUG) - results.setdefault(indexer, []).extend(t[search_term]) - except Exception as e: + results.setdefault(indexer, {}) + for term in terms: + try: + for r in t[term]: + tvdb_id = int(r['id']) + if tvdb_id not in results[indexer]: + results.setdefault(indexer, {})[tvdb_id] = r.copy() + elif r['seriesname'] != results[indexer][tvdb_id]['seriesname']: + results[indexer][tvdb_id].setdefault('aliases', []).append(r['seriesname']) + except tvdb_exception: + pass + except (StandardError, Exception): pass - # Query trakt for tvdb ids - try: - logger.log('Searching for show using search term: %s from tv datasource Trakt' % search_term, logger.DEBUG) - resp = self.getTrakt('/search/show?query=%s&extended=full' % search_term) - tvdb_ids = [] - results_trakt = [] - for item in resp: - show = item['show'] - if 'tvdb' in show['ids'] and show['ids']['tvdb'] and show['ids']['tvdb'] not in tvdb_ids: - results_trakt.append({ - 'id': show['ids']['tvdb'], 'seriesname': show['title'], - 'firstaired': (show['first_aired'] and re.sub(r'T.*$', '', str(show['first_aired'])) or show['year']), - 'network': show['network'], 'overview': show['overview'], - 'genres': ', '.join(['%s' % v.lower() for v in show.get('genres', {}) or []])}) - tvdb_ids.append(show['ids']['tvdb']) - results.update({3: results_trakt}) - if INDEXER_TVDB in results: - tvdb_filtered = [] - for tvdb_item in results[INDEXER_TVDB]: - if int(tvdb_item['id']) not in tvdb_ids: - tvdb_filtered.append(tvdb_item) - if tvdb_filtered: - results[INDEXER_TVDB] = tvdb_filtered - else: - del(results[INDEXER_TVDB]) - except: - pass + # query trakt for tvdb ids + try: + logger.log('Searching for show using search term: %s from tv datasource Trakt' % search_term, logger.DEBUG) + resp = [] + lINDEXER_API_PARMS = sickbeard.indexerApi(INDEXER_TVDB_X).api_params.copy() + lINDEXER_API_PARMS['language'] = lang + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI + lINDEXER_API_PARMS['sleep_retry'] = 5 + lINDEXER_API_PARMS['search_type'] = TraktSearchTypes.text + t = sickbeard.indexerApi(INDEXER_TVDB_X).indexer(**lINDEXER_API_PARMS) - id_names = [None, sickbeard.indexerApi(INDEXER_TVDB).name, sickbeard.indexerApi(INDEXER_TVRAGE).name, - '%s via Trakt' % sickbeard.indexerApi(INDEXER_TVDB).name] + for term in terms: + result = t[term] + resp += result + match = False + for r in result: + if isinstance(r.get('seriesname'), (str, unicode)) \ + and term.lower() == r.get('seriesname', '').lower(): + match = True + break + if match: + break + results_trakt = {} + for item in resp: + if 'tvdb' in item['ids'] and item['ids']['tvdb']: + if item['ids']['tvdb'] not in results[INDEXER_TVDB]: + results_trakt[int(item['ids']['tvdb'])] = { + 'id': item['ids']['tvdb'], 'seriesname': item['seriesname'], + 'genres': item['genres'].lower(), 'network': item['network'], + 'overview': item['overview'], 'firstaired': item['firstaired'], + 'trakt_id': item['ids']['trakt'], 'tmdb_id': item['ids']['tmdb']} + elif item['seriesname'] != results[INDEXER_TVDB][int(item['ids']['tvdb'])]['seriesname']: + results[INDEXER_TVDB][int(item['ids']['tvdb'])].setdefault( + 'aliases', []).append(item['seriesname']) + results.setdefault(INDEXER_TVDB_X, {}).update(results_trakt) + except (StandardError, Exception): + pass + + id_names = {iid: (name, '%s via %s' % (sickbeard.indexerApi(INDEXER_TVDB).name, name))[INDEXER_TVDB_X == iid] + for iid, name in sickbeard.indexerApi().all_indexers.iteritems()} + # noinspection PyUnboundLocalVariable map(final_results.extend, - ([['%s%s' % (id_names[id], helpers.findCertainShow(sickbeard.showList, int(show['id'])) and ' - exists in db' or ''), - (id, INDEXER_TVDB)[id == 3], sickbeard.indexerApi((id, INDEXER_TVDB)[id == 3]).config['show_url'], int(show['id']), + ([[id_names[iid], any([helpers.find_show_by_id( + sickbeard.showList, {(iid, INDEXER_TVDB)[INDEXER_TVDB_X == iid]: int(show['id'])}, + no_mapped_ids=False)]), + iid, (iid, INDEXER_TVDB)[INDEXER_TVDB_X == iid], + sickbeard.indexerApi((iid, INDEXER_TVDB)[INDEXER_TVDB_X == iid]).config['show_url'], int(show['id']), show['seriesname'], self.encode_html(show['seriesname']), show['firstaired'], show.get('network', '') or '', show.get('genres', '') or '', - re.sub(r'([,\.!][^,\.!]*?)$', '...', - re.sub(r'([!\?\.])(?=\w)', r'\1 ', - self.encode_html((show.get('overview', '') or '')[:250:].strip()))) - ] for show in shows] for id, shows in results.items())) + re.sub(r'([,.!][^,.!]*?)$', '...', + re.sub(r'([.!?])(?=\w)', r'\1 ', + self.encode_html((show.get('overview', '') or '')[:250:].strip()))), + self._get_UWRatio(term, show['seriesname'], show.get('aliases', [])), None, None, + self._make_search_image_url(iid, show) + ] for show in shows.itervalues()] for iid, shows in results.iteritems())) - lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] - return json.dumps({ - 'results': sorted(final_results, reverse=True, key=lambda x: dateutil.parser.parse( - re.match('^(?:19|20)\d\d$', str(x[6])) and ('%s-12-31' % str(x[6])) or (x[6] and str(x[6])) or '1900')), - 'langid': lang_id}) + def final_order(sortby_index, data, final_sort): + idx_is_indb = 1 + for (n, x) in enumerate(data): + x[sortby_index] = n + (1000, 0)[x[idx_is_indb] and 'notop' not in sickbeard.RESULTS_SORTBY] + return data if not final_sort else sorted(data, reverse=False, key=lambda x: x[sortby_index]) - def getTrakt(self, url, *args, **kwargs): + def sort_date(data_result, is_last_sort): + idx_date_sort, idx_src, idx_aired = 13, 2, 8 + return final_order( + idx_date_sort, + sorted( + sorted(data_result, reverse=True, key=lambda x: (dateutil.parser.parse( + re.match('^(?:19|20)\d\d$', str(x[idx_aired])) and ('%s-12-31' % str(x[idx_aired])) + or (x[idx_aired] and str(x[idx_aired])) or '1900'))), + reverse=False, key=lambda x: x[idx_src]), is_last_sort) - filtered = [] - try: - resp = TraktAPI().trakt_request(url, sleep_retry=5) - if len(resp): - filtered = resp - except TraktException as e: - logger.log(u'Could not connect to Trakt service: %s' % ex(e), logger.WARNING) + def sort_az(data_result, is_last_sort): + idx_az_sort, idx_src, idx_title = 14, 2, 6 + return final_order( + idx_az_sort, + sorted( + data_result, reverse=False, key=lambda x: ( + x[idx_src], + (remove_article(x[idx_title].lower()), x[idx_title].lower())[sickbeard.SORT_ARTICLE])), + is_last_sort) - return filtered + def sort_rel(data_result, is_last_sort): + idx_rel_sort, idx_src, idx_rel = 12, 2, 12 + return final_order( + idx_rel_sort, + sorted( + sorted(data_result, reverse=True, key=lambda x: x[idx_rel]), + reverse=False, key=lambda x: x[idx_src]), is_last_sort) + + if 'az' == sickbeard.RESULTS_SORTBY[:2]: + sort_results = [sort_date, sort_rel, sort_az] + elif 'date' == sickbeard.RESULTS_SORTBY[:4]: + sort_results = [sort_az, sort_rel, sort_date] + else: + sort_results = [sort_az, sort_date, sort_rel] + + for n, func in enumerate(sort_results): + final_results = func(final_results, n == len(sort_results) - 1) + + return json.dumps({'results': final_results, 'langid': sickbeard.indexerApi().config['langabbv_to_id'][lang]}) + + @staticmethod + def _make_search_image_url(iid, show): + img_url = '' + if INDEXER_TRAKT == iid: + img_url = 'imagecache?path=browse/thumb/trakt&filename=%s&trans=0&tmdbid=%s&tvdbid=%s' % \ + ('%s.jpg' % show['trakt_id'], show.get('tmdb_id'), show.get('id')) + elif INDEXER_TVDB == iid: + img_url = 'imagecache?path=browse/thumb/tvdb&filename=%s&trans=0&tvdbid=%s' % \ + ('%s.jpg' % show['id'], show['id']) + return img_url + + def _get_UWRatio(self, search_term, showname, aliases): + s = fuzz.UWRatio(search_term, showname) + # check aliases and give them a little lower score + for a in aliases: + ns = fuzz.UWRatio(search_term, a) - 1 + if ns > s: + s = ns + return s def massAddTable(self, rootDir=None, **kwargs): t = PageTemplate(headers=self.request.headers, file='home_massAddTable.tmpl') @@ -2867,7 +2975,7 @@ class NewHomeAddShows(Home): newest = dt_string img_uri = 'http://img7.anidb.net/pics/anime/%s' % image - images = dict(poster=dict(thumb='imagecache?path=anidb&source=%s' % img_uri)) + images = dict(poster=dict(thumb='imagecache?path=browse/thumb/anidb&source=%s' % img_uri)) sickbeard.CACHE_IMAGE_URL_LIST.add_url(img_uri) votes = rating = 0 @@ -3017,7 +3125,7 @@ class NewHomeAddShows(Home): dims = [row.get('poster', {}).get('width', 0), row.get('poster', {}).get('height', 0)] s = [scale(x, int(max(dims))) for x in dims] img_uri = re.sub('(?im)(.*V1_?)(\..*?)$', r'\1UX%s_CR0,0,%s,%s_AL_\2' % (s[0], s[0], s[1]), img_uri) - images = dict(poster=dict(thumb='imagecache?path=imdb&source=%s' % img_uri)) + images = dict(poster=dict(thumb='imagecache?path=browse/thumb/imdb&source=%s' % img_uri)) sickbeard.CACHE_IMAGE_URL_LIST.add_url(img_uri) filtered.append(dict( @@ -3100,7 +3208,7 @@ class NewHomeAddShows(Home): match.group(12)] img_uri = img_uri.replace(match.group(), ''.join( [str(y) for x in map(None, parts, scaled) for y in x if y is not None])) - images = dict(poster=dict(thumb='imagecache?path=imdb&source=%s' % img_uri)) + images = dict(poster=dict(thumb='imagecache?path=browse/thumb/imdb&source=%s' % img_uri)) sickbeard.CACHE_IMAGE_URL_LIST.add_url(img_uri) filtered.append(dict( @@ -3288,6 +3396,20 @@ class NewHomeAddShows(Home): 'recommendations/shows?limit=%s&' % 100, 'Recommended for %s by Trakt' % name, mode='recommended-%s' % account, send_oauth=account) + def trakt_watchlist(self, *args, **kwargs): + + if 'add' == kwargs.get('action'): + return self.redirect('/config/notifications/#tabs-3') + + account = sickbeard.helpers.tryInt(kwargs.get('account'), None) + try: + name = sickbeard.TRAKT_ACCOUNTS[account].name + except KeyError: + return self.trakt_default() + return self.browse_trakt( + 'users/%s/watchlist/shows?limit=%s&' % (sickbeard.TRAKT_ACCOUNTS[account].slug, 100), 'WatchList for %s by Trakt' % name, + mode='watchlist-%s' % account, send_oauth=account) + def trakt_default(self): return self.redirect('/home/addShows/%s' % ('trakt_trending', sickbeard.TRAKT_MRU)[any(sickbeard.TRAKT_MRU)]) @@ -3297,7 +3419,7 @@ class NewHomeAddShows(Home): browse_type = 'Trakt' normalised, filtered = ([], []) - if not sickbeard.USE_TRAKT and 'recommended' in kwargs.get('mode', ''): + if not sickbeard.USE_TRAKT and ('recommended' in kwargs.get('mode', '') or 'watchlist' in kwargs.get('mode', '')): error_msg = 'To browse personal recommendations, enable Trakt.tv in Config/Notifications/Social' return self.browse_shows(browse_type, browse_title, filtered, error_msg=error_msg, show_header=1, **kwargs) @@ -3326,6 +3448,10 @@ class NewHomeAddShows(Home): except (IndexError, KeyError): pass + if not normalised: + error_msg = 'No items in watchlist. Use the "Add to watchlist" button at the Trakt website' + return self.browse_shows(browse_type, browse_title, filtered, error_msg=error_msg, show_header=1, **kwargs) + oldest_dt = 9999999 newest_dt = 0 oldest = None @@ -3350,7 +3476,7 @@ class NewHomeAddShows(Home): tmdbid = item.get('show', {}).get('ids', {}).get('tmdb', 0) tvdbid = item.get('show', {}).get('ids', {}).get('tvdb', 0) traktid = item.get('show', {}).get('ids', {}).get('trakt', 0) - images = dict(poster=dict(thumb='imagecache?path=trakt/poster/thumb&filename=%s&tmdbid=%s&tvdbid=%s' % + images = dict(poster=dict(thumb='imagecache?path=browse/thumb/trakt&filename=%s&tmdbid=%s&tvdbid=%s' % ('%s.jpg' % traktid, tmdbid, tvdbid))) filtered.append(dict( @@ -3380,7 +3506,7 @@ class NewHomeAddShows(Home): kwargs.update(dict(oldest=oldest, newest=newest, error_msg=error_msg)) - if 'recommended' not in kwargs.get('mode', ''): + if 'recommended' not in kwargs.get('mode', '') and 'watchlist' not in kwargs.get('mode', ''): mode = kwargs.get('mode', '').split('-') if mode: func = 'trakt_%s' % mode[0] @@ -3391,6 +3517,20 @@ class NewHomeAddShows(Home): sickbeard.save_config() return self.browse_shows(browse_type, browse_title, filtered, **kwargs) + @staticmethod + def show_toggle_hide(ids): + save_config = False + for sid in ids.split(':'): + if 3 < len(sid) < 12: + save_config = True + if sid in sickbeard.BROWSELIST_HIDDEN: + sickbeard.BROWSELIST_HIDDEN.remove(sid) + else: + sickbeard.BROWSELIST_HIDDEN += [sid] + if save_config: + sickbeard.save_config() + return json.dumps({'success': save_config}) + @staticmethod def encode_html(text): @@ -3416,7 +3556,8 @@ class NewHomeAddShows(Home): t.kwargs = kwargs dedupe = [] - t.all_shows_inlibrary = 0 + t.num_inlibrary = 0 + t.num_hidden = 0 for item in shows: item['show_id'] = '' for index, tvdb in enumerate(['tvdb', 'tvrage']): @@ -3428,7 +3569,7 @@ class NewHomeAddShows(Home): # check tvshow indexer is not using the same id from another indexer if tvshow and (index + 1) == tvshow.indexer: item['show_id'] = u'%s:%s' % (tvshow.indexer, tvshow.indexerid) - t.all_shows_inlibrary += 1 + t.num_inlibrary += 1 break if None is not config.to_int(item['show_id'], None): @@ -3441,6 +3582,9 @@ class NewHomeAddShows(Home): dedupe.append(item['show_id']) t.all_shows.append(item) + if item['show_id'].split(':')[-1] in sickbeard.BROWSELIST_HIDDEN: + t.num_hidden += 1 + return t.respond() def import_shows(self, *args, **kwargs): @@ -3458,11 +3602,18 @@ class NewHomeAddShows(Home): def addNewShow(self, whichSeries=None, indexerLang='en', rootDir=None, defaultStatus=None, quality_preset=None, anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, - scene=None, blacklist=None, whitelist=None, wanted_begin=None, wanted_latest=None, tag=None): + scene=None, blacklist=None, whitelist=None, wanted_begin=None, wanted_latest=None, tag=None, + return_to=None, cancel_form=None): """ Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are provided then it forwards back to new_show, if not it goes to /home. """ + if None is not return_to: + indexer, void, indexer_id, show_name = self.split_extra_show(whichSeries) + if bool(helpers.tryInt(cancel_form)): + indexer = indexer or providedIndexer or '0' + indexer_id = re.findall('show=([\d]+)', return_to)[0] + return self.redirect(return_to % (indexer, indexer_id)) # grab our list of other dirs if given if not other_shows: @@ -3515,8 +3666,10 @@ class NewHomeAddShows(Home): # use the whole path if it's given, or else append the show name to the root dir to get the full show path if fullShowPath: show_dir = ek.ek(os.path.normpath, fullShowPath) + new_show = False else: show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) + new_show = True # blanket policy - if the dir exists you should have used 'add existing show' numbnuts if ek.ek(os.path.isdir, show_dir) and not fullShowPath: @@ -3564,7 +3717,7 @@ class NewHomeAddShows(Home): sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, int(defaultStatus), newQuality, flatten_folders, indexerLang, subtitles, anime, scene, None, blacklist, whitelist, - wanted_begin, wanted_latest, tag) # @UndefinedVariable + wanted_begin, wanted_latest, tag, new_show=new_show) # @UndefinedVariable # ui.notifications.message('Show added', 'Adding the specified show into ' + show_dir) return finishAddShow() @@ -3672,13 +3825,15 @@ class Manage(MainHandler): return t.respond() def showEpisodeStatuses(self, indexer_id, whichStatus): - status_list = [int(whichStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + whichStatus = helpers.tryInt(whichStatus) + status_list = ((([whichStatus], + Quality.SNATCHED_ANY)[SNATCHED == whichStatus], + Quality.DOWNLOADED)[DOWNLOADED == whichStatus], + Quality.ARCHIVED)[ARCHIVED == whichStatus] myDB = db.DBConnection() cur_show_results = myDB.select( - 'SELECT season, episode, name, airdate FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN (' + ','.join( + 'SELECT season, episode, name, airdate, status FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN (' + ','.join( ['?'] * len(status_list)) + ')', [int(indexer_id)] + status_list) result = {} @@ -3691,17 +3846,23 @@ class Manage(MainHandler): if cur_season not in result: result[cur_season] = {} - result[cur_season][cur_episode] = {'name': cur_result['name'], 'airdate_never': 1000 > int(cur_result['airdate'])} + cur_quality = Quality.splitCompositeStatus(int(cur_result['status']))[1] + result[cur_season][cur_episode] = {'name': cur_result['name'], + 'airdate_never': 1000 > int(cur_result['airdate']), + 'qualityCss': Quality.get_quality_css(cur_quality), + 'qualityStr': Quality.qualityStrings[cur_quality], + 'sxe': '%d x %02d' % (cur_season, cur_episode)} return json.dumps(result) def episodeStatuses(self, whichStatus=None): + whichStatus = helpers.tryInt(whichStatus) if whichStatus: - whichStatus = int(whichStatus) - status_list = [whichStatus] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + status_list = ((([whichStatus], + Quality.SNATCHED_ANY)[SNATCHED == whichStatus], + Quality.DOWNLOADED)[DOWNLOADED == whichStatus], + Quality.ARCHIVED)[ARCHIVED == whichStatus] else: status_list = [] @@ -3712,7 +3873,7 @@ class Manage(MainHandler): my_db = db.DBConnection() sql_result = my_db.select( 'SELECT COUNT(*) AS snatched FROM [tv_episodes] WHERE season > 0 AND episode > 0 AND airdate > 1 AND ' + - 'status IN (%s)' % ','.join([str(quality) for quality in Quality.SNATCHED + Quality.SNATCHED_PROPER])) + 'status IN (%s)' % ','.join([str(quality) for quality in Quality.SNATCHED_ANY])) t.default_manage = sql_result and sql_result[0]['snatched'] and SNATCHED or WANTED # if we have no status then this is as far as we need to go @@ -3722,7 +3883,7 @@ class Manage(MainHandler): status_results = my_db.select( 'SELECT show_name, tv_shows.indexer_id as indexer_id, airdate FROM tv_episodes, tv_shows WHERE tv_episodes.status IN (' + ','.join( ['?'] * len( - status_list)) + ') AND season != 0 AND tv_episodes.showid = tv_shows.indexer_id ORDER BY show_name', + status_list)) + ') AND season != 0 AND tv_episodes.showid = tv_shows.indexer_id ORDER BY show_name COLLATE NOCASE', status_list) ep_counts = {} @@ -3756,9 +3917,11 @@ class Manage(MainHandler): return t.respond() def changeEpisodeStatuses(self, oldStatus, newStatus, wantedStatus=sickbeard.common.UNKNOWN, *args, **kwargs): - status_list = [int(oldStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + status = int(oldStatus) + status_list = ((([status], + Quality.SNATCHED_ANY)[SNATCHED == status], + Quality.DOWNLOADED)[DOWNLOADED == status], + Quality.ARCHIVED)[ARCHIVED == status] to_change = {} @@ -4353,15 +4516,15 @@ class Manage(MainHandler): class ManageSearches(Manage): def index(self, *args, **kwargs): t = PageTemplate(headers=self.request.headers, file='manage_manageSearches.tmpl') - # t.backlogPI = sickbeard.backlogSearchScheduler.action.get_progress_indicator() - t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() - t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() - t.backlogIsActive = sickbeard.backlogSearchScheduler.action.am_running() - t.standardBacklogRunning = sickbeard.searchQueueScheduler.action.is_standard_backlog_in_progress() - t.backlogRunningType = sickbeard.searchQueueScheduler.action.type_of_backlog_in_progress() - t.recentSearchStatus = sickbeard.searchQueueScheduler.action.is_recentsearch_in_progress() - t.findPropersStatus = sickbeard.searchQueueScheduler.action.is_propersearch_in_progress() - t.queueLength = sickbeard.searchQueueScheduler.action.queue_length() + # t.backlog_pi = sickbeard.backlogSearchScheduler.action.get_progress_indicator() + t.backlog_paused = sickbeard.searchQueueScheduler.action.is_backlog_paused() + t.backlog_running = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() + t.backlog_is_active = sickbeard.backlogSearchScheduler.action.am_running() + t.standard_backlog_running = sickbeard.searchQueueScheduler.action.is_standard_backlog_in_progress() + t.backlog_running_type = sickbeard.searchQueueScheduler.action.type_of_backlog_in_progress() + t.recent_search_status = sickbeard.searchQueueScheduler.action.is_recentsearch_in_progress() + t.find_propers_status = sickbeard.searchQueueScheduler.action.is_propersearch_in_progress() + t.queue_length = sickbeard.searchQueueScheduler.action.queue_length() t.submenu = self.ManageMenu('Search') @@ -4420,9 +4583,22 @@ class ManageSearches(Manage): class showProcesses(Manage): def index(self, *args, **kwargs): t = PageTemplate(headers=self.request.headers, file='manage_showProcesses.tmpl') - t.queueLength = sickbeard.showQueueScheduler.action.queue_length() - t.showList = sickbeard.showList - t.ShowUpdateRunning = sickbeard.showQueueScheduler.action.isShowUpdateRunning() or sickbeard.showUpdateScheduler.action.amActive + t.queue_length = sickbeard.showQueueScheduler.action.queue_length() + t.show_list = sickbeard.showList + t.show_update_running = sickbeard.showQueueScheduler.action.isShowUpdateRunning() or sickbeard.showUpdateScheduler.action.amActive + + myDb = db.DBConnection(row_type='dict') + sql_results = myDb.select('SELECT n.indexer, n.indexer_id, n.last_success, n.fail_count, s.show_name FROM tv_shows_not_found as n INNER JOIN tv_shows as s ON (n.indexer == s.indexer AND n.indexer_id == s.indexer_id)') + for s in sql_results: + date = helpers.tryInt(s['last_success']) + s['last_success'] = ('never', sbdatetime.sbdatetime.fromordinal(date).sbfdate())[date > 1] + s['ignore_warning'] = 0 > s['fail_count'] + defunct_indexer = [i for i in sickbeard.indexerApi().all_indexers if sickbeard.indexerApi(i).config.get('defunct')] + sql_r = None + if defunct_indexer: + sql_r = myDb.select('SELECT indexer, indexer_id, show_name FROM tv_shows WHERE indexer IN (%s)' % ','.join(['?'] * len(defunct_indexer)), defunct_indexer) + t.defunct_indexer = sql_r + t.not_found_shows = sql_results t.submenu = self.ManageMenu('Processes') @@ -4438,6 +4614,28 @@ class showProcesses(Manage): time.sleep(5) self.redirect('/manage/showProcesses/') + @staticmethod + def switch_ignore_warning(*args, **kwargs): + + for k, v in kwargs.iteritems(): + try: + indexer_id, state = k.split('|') + except ValueError: + continue + indexer, indexer_id = helpers.tryInt(v), helpers.tryInt(indexer_id) + if 0 < indexer and 0 < indexer_id: + show_obj = helpers.find_show_by_id(sickbeard.showList, {indexer: indexer_id}) + if show_obj: + change = -1 + if 'true' == state: + if 0 > show_obj.not_found_count: + change = 1 + elif 0 < show_obj.not_found_count: + change = 1 + show_obj.not_found_count *= change + + return json.dumps({}) + class History(MainHandler): def index(self, limit=100): @@ -4562,6 +4760,19 @@ class ConfigGeneral(Config): def saveRootDirs(self, rootDirString=None): sickbeard.ROOT_DIRS = rootDirString + def saveResultPrefs(self, ui_results_sortby=None): + + if ui_results_sortby in ('az', 'date', 'rel', 'notop', 'ontop'): + was_ontop = 'notop' not in sickbeard.RESULTS_SORTBY + if 'top' == ui_results_sortby[-3:]: + maybe_ontop = ('', ' notop')[was_ontop] + sortby = sickbeard.RESULTS_SORTBY.replace(' notop', '') + sickbeard.RESULTS_SORTBY = '%s%s' % (('rel', sortby)[any([sortby])], maybe_ontop) + else: + sickbeard.RESULTS_SORTBY = '%s%s' % (ui_results_sortby, (' notop', '')[was_ontop]) + + sickbeard.save_config() + def saveAddShowDefaults(self, default_status, any_qualities='', best_qualities='', default_wanted_begin=None, default_wanted_latest=None, default_flatten_folders=False, default_scene=False, default_subtitles=False, default_anime=False, default_tag=''): @@ -4604,7 +4815,7 @@ class ConfigGeneral(Config): logger.log(u'New API generated') return m.hexdigest() - def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, + def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, web_ipv64=None, update_shows_on_start=None, show_update_hour=None, trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, web_username=None, use_api=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None, file_logging_preset=None, @@ -4621,6 +4832,11 @@ class ConfigGeneral(Config): sickbeard.LAUNCH_BROWSER = config.checkbox_to_value(launch_browser) sickbeard.UPDATE_SHOWS_ON_START = config.checkbox_to_value(update_shows_on_start) sickbeard.SHOW_UPDATE_HOUR = config.minimax(show_update_hour, 3, 0, 23) + try: + with sickbeard.showUpdateScheduler.lock: + sickbeard.showUpdateScheduler.start_time = datetime.time(hour=sickbeard.SHOW_UPDATE_HOUR) + except (StandardError, Exception) as e: + logger.log('Could not change Show Update Scheduler time: %s' % ex(e), logger.ERROR) sickbeard.TRASH_REMOVE_SHOW = config.checkbox_to_value(trash_remove_show) sickbeard.TRASH_ROTATE_LOGS = config.checkbox_to_value(trash_rotate_logs) if not config.change_LOG_DIR(log_dir, web_log): @@ -4694,6 +4910,7 @@ class ConfigGeneral(Config): 'Unable to create directory ' + os.path.normpath(https_key) + ', https key directory not changed.'] sickbeard.WEB_IPV6 = config.checkbox_to_value(web_ipv6) + sickbeard.WEB_IPV64 = config.checkbox_to_value(web_ipv64) sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) # Advanced @@ -4766,11 +4983,12 @@ class ConfigSearch(Config): nzbget_category=None, nzbget_priority=None, nzbget_host=None, nzbget_use_https=None, backlog_days=None, backlog_frequency=None, search_unaired=None, unaired_recent_search_only=None, recentsearch_frequency=None, nzb_method=None, torrent_method=None, usenet_retention=None, - download_propers=None, check_propers_interval=None, allow_high_priority=None, + download_propers=None, propers_webdl_onegrp=None, check_propers_interval=None, + allow_high_priority=None, torrent_dir=None, torrent_username=None, torrent_password=None, torrent_host=None, torrent_label=None, torrent_path=None, torrent_verify_cert=None, - torrent_seed_time=None, torrent_paused=None, torrent_high_bandwidth=None, ignore_words=None, require_words=None, - backlog_nofull=None): + torrent_seed_time=None, torrent_paused=None, torrent_high_bandwidth=None, + ignore_words=None, require_words=None, backlog_nofull=None): results = [] @@ -4803,6 +5021,7 @@ class ConfigSearch(Config): sickbeard.REQUIRE_WORDS = require_words if require_words else '' sickbeard.DOWNLOAD_PROPERS = config.checkbox_to_value(download_propers) + sickbeard.PROPERS_WEBDL_ONEGRP = config.checkbox_to_value(propers_webdl_onegrp) if sickbeard.CHECK_PROPERS_INTERVAL != check_propers_interval: sickbeard.CHECK_PROPERS_INTERVAL = check_propers_interval @@ -5121,16 +5340,19 @@ class ConfigProviders(Config): return json.dumps({'success': False, 'error': error}) if name in [n.name for n in sickbeard.newznabProviderList if n.url == url]: - tv_categories = newznab.NewznabProvider.clean_newznab_categories([n for n in sickbeard.newznabProviderList if n.name == name][0].all_cats) + provider = [n for n in sickbeard.newznabProviderList if n.name == name][0] + tv_categories = provider.clean_newznab_categories(provider.all_cats) + state = provider.is_enabled() else: providers = dict(zip([x.get_id() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) temp_provider = newznab.NewznabProvider(name, url, key) if None is not key and starify(key, True): temp_provider.key = providers[temp_provider.get_id()].key - tv_categories = newznab.NewznabProvider.clean_newznab_categories(temp_provider.all_cats) + tv_categories = temp_provider.clean_newznab_categories(temp_provider.all_cats) + state = False - return json.dumps({'success': True, 'tv_categories': tv_categories, 'error': ''}) + return json.dumps({'success': True, 'tv_categories': tv_categories, 'state': state, 'error': ''}) def deleteNewznabProvider(self, nnid): @@ -5201,6 +5423,35 @@ class ConfigProviders(Config): return '1' + def checkProvidersPing(self): + for p in sickbeard.providers.sortedProviderList(): + if getattr(p, 'ping_freq', None): + if p.is_active() and (p.get_id() not in sickbeard.provider_ping_thread_pool + or not sickbeard.provider_ping_thread_pool[p.get_id()].is_alive()): + # noinspection PyProtectedMember + sickbeard.provider_ping_thread_pool[p.get_id()] = threading.Thread( + name='PING-PROVIDER %s' % p.name, target=p._ping) + sickbeard.provider_ping_thread_pool[p.get_id()].start() + elif not p.is_active() and p.get_id() in sickbeard.provider_ping_thread_pool: + sickbeard.provider_ping_thread_pool[p.get_id()].stop = True + try: + sickbeard.provider_ping_thread_pool[p.get_id()].join(120) + if not sickbeard.provider_ping_thread_pool[p.get_id()].is_alive(): + sickbeard.provider_ping_thread_pool.pop(p.get_id()) + except RuntimeError: + pass + + # stop removed providers + prov = [n.get_id() for n in sickbeard.providers.sortedProviderList()] + for p in [x for x in sickbeard.provider_ping_thread_pool if x not in prov]: + sickbeard.provider_ping_thread_pool[p].stop = True + try: + sickbeard.provider_ping_thread_pool[p].join(120) + if not sickbeard.provider_ping_thread_pool[p].is_alive(): + sickbeard.provider_ping_thread_pool.pop(p) + except RuntimeError: + pass + def saveProviders(self, newznab_string='', torrentrss_string='', provider_order=None, **kwargs): results = [] @@ -5243,7 +5494,7 @@ class ConfigProviders(Config): if cur_id + '_' + attr in kwargs: setattr(nzb_src, attr, str(kwargs.get(cur_id + '_' + attr)).strip()) - for attr in ['search_fallback', 'enable_recentsearch', 'enable_backlog']: + for attr in ['search_fallback', 'enable_recentsearch', 'enable_backlog', 'enable_scheduled_backlog']: setattr(nzb_src, attr, config.checkbox_to_value(kwargs.get(cur_id + '_' + attr))) else: @@ -5252,8 +5503,9 @@ class ConfigProviders(Config): active_ids.append(cur_id) # delete anything that is missing - for source in [x for x in sickbeard.newznabProviderList if x.get_id() not in active_ids]: - sickbeard.newznabProviderList.remove(source) + if sickbeard.USE_NZBS: + for source in [x for x in sickbeard.newznabProviderList if x.get_id() not in active_ids]: + sickbeard.newznabProviderList.remove(source) # add all the torrent RSS info we have into our list torrent_rss_sources = dict(zip([x.get_id() for x in sickbeard.torrentRssProviderList], @@ -5287,8 +5539,9 @@ class ConfigProviders(Config): active_ids.append(cur_id) # delete anything that is missing - for source in [x for x in sickbeard.torrentRssProviderList if x.get_id() not in active_ids]: - sickbeard.torrentRssProviderList.remove(source) + if sickbeard.USE_TORRENTS: + for source in [x for x in sickbeard.torrentRssProviderList if x.get_id() not in active_ids]: + sickbeard.torrentRssProviderList.remove(source) # enable/disable states of source providers provider_str_list = provider_order.split() @@ -5300,12 +5553,18 @@ class ConfigProviders(Config): provider_list.append(src_name) src_enabled = bool(config.to_int(src_enabled)) - if '' != getattr(sources[src_name], 'enabled', '') and sources[src_name].is_enabled() != src_enabled: + if src_name in sources and '' != getattr(sources[src_name], 'enabled', '') \ + and sources[src_name].is_enabled() != src_enabled: + if isinstance(sources[src_name], sickbeard.providers.newznab.NewznabProvider) and \ + not sources[src_name].enabled and src_enabled: + reload_page = True sources[src_name].enabled = src_enabled if not reload_page and sickbeard.GenericProvider.TORRENT == sources[src_name].providerType: reload_page = True if src_name in newznab_sources: + if not newznab_sources[src_name].enabled and src_enabled: + reload_page = True newznab_sources[src_name].enabled = src_enabled elif src_name in torrent_rss_sources: torrent_rss_sources[src_name].enabled = src_enabled @@ -5339,7 +5598,7 @@ class ConfigProviders(Config): setattr(torrent_src, attr, config.to_int(str(kwargs.get(src_id_prefix + attr)).strip())) for attr in [x for x in ['confirmed', 'freeleech', 'reject_m2ts', 'enable_recentsearch', - 'enable_backlog', 'search_fallback'] + 'enable_backlog', 'search_fallback', 'enable_scheduled_backlog'] if hasattr(torrent_src, x) and src_id_prefix + attr in kwargs]: setattr(torrent_src, attr, config.checkbox_to_value(kwargs.get(src_id_prefix + attr))) @@ -5381,7 +5640,7 @@ class ConfigProviders(Config): setattr(nzb_src, attr, config.checkbox_to_value(kwargs.get(src_id_prefix + attr)) or not getattr(nzb_src, 'supports_backlog', True)) - for attr in [x for x in ['search_fallback', 'enable_backlog'] if hasattr(nzb_src, x)]: + for attr in [x for x in ['search_fallback', 'enable_backlog', 'enable_scheduled_backlog'] if hasattr(nzb_src, x)]: setattr(nzb_src, attr, config.checkbox_to_value(kwargs.get(src_id_prefix + attr))) sickbeard.NEWZNAB_DATA = '!!!'.join([x.config_str() for x in sickbeard.newznabProviderList]) @@ -5391,6 +5650,9 @@ class ConfigProviders(Config): sickbeard.save_config() + cp = threading.Thread(name='Check-Ping-Providers', target=self.checkProvidersPing) + cp.start() + if 0 < len(results): for x in results: logger.log(x, logger.ERROR) @@ -5419,54 +5681,62 @@ class ConfigNotifications(Config): 'b64': base64.urlsafe_b64encode(location)}) return t.respond() - def saveNotifications(self, - use_emby=None, emby_update_library=None, emby_host=None, emby_apikey=None, - use_kodi=None, kodi_always_on=None, kodi_notify_onsnatch=None, kodi_notify_ondownload=None, - kodi_notify_onsubtitledownload=None, kodi_update_onlyfirst=None, - kodi_update_library=None, kodi_update_full=None, - kodi_host=None, kodi_username=None, kodi_password=None, - use_xbmc=None, xbmc_always_on=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, - xbmc_notify_onsubtitledownload=None, xbmc_update_onlyfirst=None, - xbmc_update_library=None, xbmc_update_full=None, - xbmc_host=None, xbmc_username=None, xbmc_password=None, - use_plex=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, - plex_notify_onsubtitledownload=None, plex_update_library=None, - plex_server_host=None, plex_host=None, plex_username=None, plex_password=None, - use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, - growl_notify_onsubtitledownload=None, growl_host=None, growl_password=None, - use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, - prowl_notify_onsubtitledownload=None, prowl_api=None, prowl_priority=0, - use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, - twitter_notify_onsubtitledownload=None, - use_boxcar2=None, boxcar2_notify_onsnatch=None, boxcar2_notify_ondownload=None, - boxcar2_notify_onsubtitledownload=None, boxcar2_accesstoken=None, boxcar2_sound=None, - use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, - pushover_notify_onsubtitledownload=None, pushover_userkey=None, pushover_apikey=None, - pushover_priority=None, pushover_device=None, pushover_sound=None, pushover_device_list=None, - use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, - libnotify_notify_onsubtitledownload=None, - use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, - use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, - use_trakt=None, trakt_pin=None, - trakt_remove_watchlist=None, trakt_use_watchlist=None, trakt_method_add=None, - trakt_start_paused=None, trakt_sync=None, - trakt_default_indexer=None, trakt_remove_serieslist=None, trakt_collection=None, trakt_accounts=None, - use_synologynotifier=None, synologynotifier_notify_onsnatch=None, - synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None, - use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, - pytivo_notify_onsubtitledownload=None, pytivo_update_library=None, - pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, - use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, - nma_notify_onsubtitledownload=None, nma_api=None, nma_priority=0, - use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, - pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, - use_pushbullet=None, pushbullet_notify_onsnatch=None, pushbullet_notify_ondownload=None, - pushbullet_notify_onsubtitledownload=None, pushbullet_access_token=None, - pushbullet_device_iden=None, pushbullet_device_list=None, - use_email=None, email_notify_onsnatch=None, email_notify_ondownload=None, - email_notify_onsubtitledownload=None, email_host=None, email_port=25, email_from=None, - email_tls=None, email_user=None, email_password=None, email_list=None, email_show_list=None, - email_show=None, **kwargs): + def save_notifications( + self, + use_emby=None, emby_update_library=None, emby_host=None, emby_apikey=None, + use_kodi=None, kodi_always_on=None, kodi_update_library=None, kodi_update_full=None, + kodi_update_onlyfirst=None, kodi_host=None, kodi_username=None, kodi_password=None, + kodi_notify_onsnatch=None, kodi_notify_ondownload=None, kodi_notify_onsubtitledownload=None, + use_plex=None, plex_update_library=None, plex_username=None, plex_password=None, plex_server_host=None, + plex_notify_onsnatch=None, plex_notify_ondownload=None, plex_notify_onsubtitledownload=None, plex_host=None, + # use_xbmc=None, xbmc_always_on=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, + # xbmc_notify_onsubtitledownload=None, xbmc_update_onlyfirst=None, + # xbmc_update_library=None, xbmc_update_full=None, + # xbmc_host=None, xbmc_username=None, xbmc_password=None, + use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, + use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, + use_synoindex=None, use_synologynotifier=None, synologynotifier_notify_onsnatch=None, + synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None, + use_pytivo=None, pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, + + use_boxcar2=None, boxcar2_notify_onsnatch=None, boxcar2_notify_ondownload=None, + boxcar2_notify_onsubtitledownload=None, boxcar2_access_token=None, boxcar2_sound=None, + use_pushbullet=None, pushbullet_notify_onsnatch=None, pushbullet_notify_ondownload=None, + pushbullet_notify_onsubtitledownload=None, pushbullet_access_token=None, pushbullet_device_iden=None, + use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, + pushover_notify_onsubtitledownload=None, pushover_userkey=None, pushover_apikey=None, + pushover_priority=None, pushover_device=None, pushover_sound=None, pushover_device_list=None, + use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, + growl_notify_onsubtitledownload=None, growl_host=None, growl_password=None, + use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, + prowl_notify_onsubtitledownload=None, prowl_api=None, prowl_priority=0, + use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, + nma_notify_onsubtitledownload=None, nma_api=None, nma_priority=0, + use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, + libnotify_notify_onsubtitledownload=None, + # use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, + # pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, + + use_trakt=None, + # trakt_pin=None, trakt_remove_watchlist=None, trakt_use_watchlist=None, trakt_method_add=None, + # trakt_start_paused=None, trakt_sync=None, trakt_default_indexer=None, trakt_remove_serieslist=None, + # trakt_collection=None, trakt_accounts=None, + use_slack=None, slack_notify_onsnatch=None, slack_notify_ondownload=None, + slack_notify_onsubtitledownload=None, slack_access_token=None, slack_channel=None, + slack_as_authed=None, slack_bot_name=None, slack_icon_url=None, + use_discordapp=None, discordapp_notify_onsnatch=None, discordapp_notify_ondownload=None, + discordapp_notify_onsubtitledownload=None, discordapp_access_token=None, + discordapp_as_authed=None, discordapp_username=None, discordapp_icon_url=None, + discordapp_as_tts=None, + use_gitter=None, gitter_notify_onsnatch=None, gitter_notify_ondownload=None, + gitter_notify_onsubtitledownload=None, gitter_access_token=None, gitter_room=None, + use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, + twitter_notify_onsubtitledownload=None, + use_email=None, email_notify_onsnatch=None, email_notify_ondownload=None, + email_notify_onsubtitledownload=None, email_host=None, email_port=25, email_from=None, + email_tls=None, email_user=None, email_password=None, email_list=None, + # email_show_list=None, email_show=None, + **kwargs): results = [] @@ -5502,18 +5772,18 @@ class ConfigNotifications(Config): if set('*') != set(kodi_password): sickbeard.KODI_PASSWORD = kodi_password - sickbeard.USE_XBMC = config.checkbox_to_value(use_xbmc) - sickbeard.XBMC_ALWAYS_ON = config.checkbox_to_value(xbmc_always_on) - sickbeard.XBMC_NOTIFY_ONSNATCH = config.checkbox_to_value(xbmc_notify_onsnatch) - sickbeard.XBMC_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(xbmc_notify_ondownload) - sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(xbmc_notify_onsubtitledownload) - sickbeard.XBMC_UPDATE_LIBRARY = config.checkbox_to_value(xbmc_update_library) - sickbeard.XBMC_UPDATE_FULL = config.checkbox_to_value(xbmc_update_full) - sickbeard.XBMC_UPDATE_ONLYFIRST = config.checkbox_to_value(xbmc_update_onlyfirst) - sickbeard.XBMC_HOST = config.clean_hosts(xbmc_host) - sickbeard.XBMC_USERNAME = xbmc_username - if set('*') != set(xbmc_password): - sickbeard.XBMC_PASSWORD = xbmc_password + # sickbeard.USE_XBMC = config.checkbox_to_value(use_xbmc) + # sickbeard.XBMC_ALWAYS_ON = config.checkbox_to_value(xbmc_always_on) + # sickbeard.XBMC_NOTIFY_ONSNATCH = config.checkbox_to_value(xbmc_notify_onsnatch) + # sickbeard.XBMC_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(xbmc_notify_ondownload) + # sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(xbmc_notify_onsubtitledownload) + # sickbeard.XBMC_UPDATE_LIBRARY = config.checkbox_to_value(xbmc_update_library) + # sickbeard.XBMC_UPDATE_FULL = config.checkbox_to_value(xbmc_update_full) + # sickbeard.XBMC_UPDATE_ONLYFIRST = config.checkbox_to_value(xbmc_update_onlyfirst) + # sickbeard.XBMC_HOST = config.clean_hosts(xbmc_host) + # sickbeard.XBMC_USERNAME = xbmc_username + # if set('*') != set(xbmc_password): + # sickbeard.XBMC_PASSWORD = xbmc_password sickbeard.USE_PLEX = config.checkbox_to_value(use_plex) sickbeard.PLEX_NOTIFY_ONSNATCH = config.checkbox_to_value(plex_notify_onsnatch) @@ -5552,7 +5822,7 @@ class ConfigNotifications(Config): sickbeard.BOXCAR2_NOTIFY_ONSNATCH = config.checkbox_to_value(boxcar2_notify_onsnatch) sickbeard.BOXCAR2_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(boxcar2_notify_ondownload) sickbeard.BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(boxcar2_notify_onsubtitledownload) - key = boxcar2_accesstoken.strip() + key = boxcar2_access_token.strip() if not starify(key, True): sickbeard.BOXCAR2_ACCESSTOKEN = key sickbeard.BOXCAR2_SOUND = boxcar2_sound @@ -5595,8 +5865,8 @@ class ConfigNotifications(Config): synologynotifier_notify_onsubtitledownload) sickbeard.USE_TRAKT = config.checkbox_to_value(use_trakt) - # sickbeard.traktCheckerScheduler.silent = not sickbeard.USE_TRAKT sickbeard.TRAKT_UPDATE_COLLECTION = build_config(**kwargs) + # sickbeard.traktCheckerScheduler.silent = not sickbeard.USE_TRAKT # sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer) # sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync) # sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist) @@ -5605,6 +5875,33 @@ class ConfigNotifications(Config): # sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) # sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused) + sickbeard.USE_SLACK = config.checkbox_to_value(use_slack) + sickbeard.SLACK_NOTIFY_ONSNATCH = config.checkbox_to_value(slack_notify_onsnatch) + sickbeard.SLACK_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(slack_notify_ondownload) + sickbeard.SLACK_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(slack_notify_onsubtitledownload) + sickbeard.SLACK_ACCESS_TOKEN = slack_access_token + sickbeard.SLACK_CHANNEL = slack_channel + sickbeard.SLACK_AS_AUTHED = config.checkbox_to_value(slack_as_authed) + sickbeard.SLACK_BOT_NAME = slack_bot_name + sickbeard.SLACK_ICON_URL = slack_icon_url + + sickbeard.USE_DISCORDAPP = config.checkbox_to_value(use_discordapp) + sickbeard.DISCORDAPP_NOTIFY_ONSNATCH = config.checkbox_to_value(discordapp_notify_onsnatch) + sickbeard.DISCORDAPP_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(discordapp_notify_ondownload) + sickbeard.DISCORDAPP_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(discordapp_notify_onsubtitledownload) + sickbeard.DISCORDAPP_ACCESS_TOKEN = discordapp_access_token + sickbeard.DISCORDAPP_AS_AUTHED = config.checkbox_to_value(discordapp_as_authed) + sickbeard.DISCORDAPP_USERNAME = discordapp_username + sickbeard.DISCORDAPP_ICON_URL = discordapp_icon_url + sickbeard.DISCORDAPP_AS_TTS = config.checkbox_to_value(discordapp_as_tts) + + sickbeard.USE_GITTER = config.checkbox_to_value(use_gitter) + sickbeard.GITTER_NOTIFY_ONSNATCH = config.checkbox_to_value(gitter_notify_onsnatch) + sickbeard.GITTER_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(gitter_notify_ondownload) + sickbeard.GITTER_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(gitter_notify_onsubtitledownload) + sickbeard.GITTER_ACCESS_TOKEN = gitter_access_token + sickbeard.GITTER_ROOM = gitter_room + sickbeard.USE_EMAIL = config.checkbox_to_value(use_email) sickbeard.EMAIL_NOTIFY_ONSNATCH = config.checkbox_to_value(email_notify_onsnatch) sickbeard.EMAIL_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(email_notify_ondownload) @@ -5619,10 +5916,6 @@ class ConfigNotifications(Config): sickbeard.EMAIL_LIST = email_list sickbeard.USE_PYTIVO = config.checkbox_to_value(use_pytivo) - sickbeard.PYTIVO_NOTIFY_ONSNATCH = config.checkbox_to_value(pytivo_notify_onsnatch) - sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pytivo_notify_ondownload) - sickbeard.PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pytivo_notify_onsubtitledownload) - sickbeard.PYTIVO_UPDATE_LIBRARY = config.checkbox_to_value(pytivo_update_library) sickbeard.PYTIVO_HOST = config.clean_host(pytivo_host) sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name @@ -5636,13 +5929,13 @@ class ConfigNotifications(Config): sickbeard.NMA_API = key sickbeard.NMA_PRIORITY = nma_priority - sickbeard.USE_PUSHALOT = config.checkbox_to_value(use_pushalot) - sickbeard.PUSHALOT_NOTIFY_ONSNATCH = config.checkbox_to_value(pushalot_notify_onsnatch) - sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushalot_notify_ondownload) - sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushalot_notify_onsubtitledownload) - key = pushalot_authorizationtoken.strip() - if not starify(key, True): - sickbeard.PUSHALOT_AUTHORIZATIONTOKEN = key + # sickbeard.USE_PUSHALOT = config.checkbox_to_value(use_pushalot) + # sickbeard.PUSHALOT_NOTIFY_ONSNATCH = config.checkbox_to_value(pushalot_notify_onsnatch) + # sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushalot_notify_ondownload) + # sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushalot_notify_onsubtitledownload) + # key = pushalot_authorizationtoken.strip() + # if not starify(key, True): + # sickbeard.PUSHALOT_AUTHORIZATIONTOKEN = key sickbeard.USE_PUSHBULLET = config.checkbox_to_value(use_pushbullet) sickbeard.PUSHBULLET_NOTIFY_ONSNATCH = config.checkbox_to_value(pushbullet_notify_onsnatch) @@ -5679,19 +5972,7 @@ class ConfigSubtitles(Config): if subtitles_finder_frequency == '' or subtitles_finder_frequency is None: subtitles_finder_frequency = 1 - if use_subtitles == 'on' and not sickbeard.subtitlesFinderScheduler.isAlive(): - sickbeard.subtitlesFinderScheduler.silent = False - sickbeard.subtitlesFinderScheduler.start() - else: - sickbeard.subtitlesFinderScheduler.stop.set() - sickbeard.subtitlesFinderScheduler.silent = True - logger.log(u'Waiting for the SUBTITLESFINDER thread to exit') - try: - sickbeard.subtitlesFinderScheduler.join(5) - except: - pass - - sickbeard.USE_SUBTITLES = config.checkbox_to_value(use_subtitles) + config.change_USE_SUBTITLES(config.checkbox_to_value(use_subtitles)) sickbeard.SUBTITLES_LANGUAGES = [lang.alpha2 for lang in subtitles.isValidLanguage( subtitles_languages.replace(' ', '').split(','))] if subtitles_languages != '' else '' sickbeard.SUBTITLES_DIR = subtitles_dir @@ -5993,26 +6274,33 @@ class CachedImages(MainHandler): tmdbimage = False if source is not None and source in sickbeard.CACHE_IMAGE_URL_LIST: s = source - if source is None and tmdbid not in [None, 0, '0'] and self.should_try_image(static_image_path, 'tmdb'): + if source is None and tmdbid not in [None, 'None', 0, '0'] \ + and self.should_try_image(static_image_path, 'tmdb'): tmdbimage = True try: tmdbapi = TMDB(sickbeard.TMDB_API_KEY) tmdbconfig = tmdbapi.Configuration().info() images = tmdbapi.TV(helpers.tryInt(tmdbid)).images() - s = '%s%s%s' % (tmdbconfig['images']['base_url'], tmdbconfig['images']['poster_sizes'][3], sorted(images['posters'], key=lambda x: x['vote_average'], reverse=True)[0]['file_path']) if len(images['posters']) > 0 else '' - except: + s = '%s%s%s' % (tmdbconfig['images']['base_url'], tmdbconfig['images']['poster_sizes'][3], + sorted(images['posters'], key=lambda x: x['vote_average'], + reverse=True)[0]['file_path']) if len(images['posters']) > 0 else '' + except (StandardError, Exception): s = '' if s and not helpers.download_file(s, static_image_path) and s.find('trakt.us'): helpers.download_file(s.replace('trakt.us', 'trakt.tv'), static_image_path) if tmdbimage and not ek.ek(os.path.isfile, static_image_path): self.create_dummy_image(static_image_path, 'tmdb') - if source is None and tvdbid not in [None, 0, '0'] and not ek.ek(os.path.isfile, static_image_path) and self.should_try_image(static_image_path, 'tvdb'): + if source is None and tvdbid not in [None, 'None', 0, '0'] \ + and not ek.ek(os.path.isfile, static_image_path) \ + and self.should_try_image(static_image_path, 'tvdb'): try: - r = sickbeard.indexerApi(INDEXER_TVDB).indexer()[helpers.tryInt(tvdbid), False] + lINDEXER_API_PARMS = sickbeard.indexerApi(INDEXER_TVDB).api_params.copy() + lINDEXER_API_PARMS['posters'] = True + r = sickbeard.indexerApi(INDEXER_TVDB).indexer(**lINDEXER_API_PARMS)[helpers.tryInt(tvdbid), False] if hasattr(r, 'data') and 'poster' in r.data: s = r.data['poster'] - except: + except (StandardError, Exception): s = '' if s: helpers.download_file(s, static_image_path) @@ -6023,7 +6311,12 @@ class CachedImages(MainHandler): self.delete_all_dummy_images(static_image_path) if not ek.ek(os.path.isfile, static_image_path): - self.redirect('images/trans.png') + static_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', + 'images', ('image-light.png', 'trans.png')[bool(int(kwargs.get('trans', 1)))]) else: helpers.set_file_timestamp(static_image_path, min_age=3, new_time=None) - self.redirect('cache/images/%s/%s' % (path, file_name)) + + mime_type, encoding = MimeTypes().guess_type(static_image_path) + self.set_header('Content-Type', mime_type) + with open(static_image_path, 'rb') as img: + return img.read() diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 0d4cc10b..9613082e 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -19,6 +19,7 @@ class WebServer(threading.Thread): self.alive = True self.name = 'TORNADO' self.io_loop = io_loop or IOLoop.current() + self.server = None self.options = options self.options.setdefault('port', 8081) @@ -31,8 +32,7 @@ class WebServer(threading.Thread): assert 'data_root' in self.options # web root - self.options['web_root'] = ('/' + self.options['web_root'].lstrip('/')) if self.options[ - 'web_root'] else '' + self.options['web_root'] = ('/' + self.options['web_root'].lstrip('/')) if self.options['web_root'] else '' # tornado setup self.enable_https = self.options['enable_https'] @@ -58,38 +58,8 @@ class WebServer(threading.Thread): debug=True, autoreload=False, gzip=True, - xheaders=sickbeard.HANDLE_REVERSE_PROXY, cookie_secret=sickbeard.COOKIE_SECRET, - login_url='%s/login/' % self.options['web_root'] - ) - - # Main Handler - self.app.add_handlers('.*$', [ - (r'%s/api/builder(/?)(.*)' % self.options['web_root'], webserve.ApiBuilder), - (r'%s/api(/?.*)' % self.options['web_root'], webapi.Api), - (r'%s/imagecache(/?.*)' % self.options['web_root'], webserve.CachedImages), - (r'%s/cache(/?.*)' % self.options['web_root'], webserve.Cache), - (r'%s/config/general(/?.*)' % self.options['web_root'], webserve.ConfigGeneral), - (r'%s/config/search(/?.*)' % self.options['web_root'], webserve.ConfigSearch), - (r'%s/config/providers(/?.*)' % self.options['web_root'], webserve.ConfigProviders), - (r'%s/config/subtitles(/?.*)' % self.options['web_root'], webserve.ConfigSubtitles), - (r'%s/config/postProcessing(/?.*)' % self.options['web_root'], webserve.ConfigPostProcessing), - (r'%s/config/notifications(/?.*)' % self.options['web_root'], webserve.ConfigNotifications), - (r'%s/config/anime(/?.*)' % self.options['web_root'], webserve.ConfigAnime), - (r'%s/config(/?.*)' % self.options['web_root'], webserve.Config), - (r'%s/errorlogs(/?.*)' % self.options['web_root'], webserve.ErrorLogs), - (r'%s/history(/?.*)' % self.options['web_root'], webserve.History), - (r'%s/home/is_alive(/?.*)' % self.options['web_root'], webserve.IsAliveHandler), - (r'%s/home/addShows(/?.*)' % self.options['web_root'], webserve.NewHomeAddShows), - (r'%s/home/postprocess(/?.*)' % self.options['web_root'], webserve.HomePostProcess), - (r'%s/home(/?.*)' % self.options['web_root'], webserve.Home), - (r'%s/manage/manageSearches(/?.*)' % self.options['web_root'], webserve.ManageSearches), - (r'%s/manage/showProcesses(/?.*)' % self.options['web_root'], webserve.showProcesses), - (r'%s/manage/(/?.*)' % self.options['web_root'], webserve.Manage), - (r'%s/ui(/?.*)' % self.options['web_root'], webserve.UI), - (r'%s/browser(/?.*)' % self.options['web_root'], webserve.WebFileBrowser), - (r'%s(/?.*)' % self.options['web_root'], webserve.MainHandler), - ]) + login_url='%s/login/' % self.options['web_root']) # webui login/logout handlers self.app.add_handlers('.*$', [ @@ -125,20 +95,45 @@ class WebServer(threading.Thread): {'path': os.path.join(self.options['data_root'], 'js')}), ]) + # Main Handler + self.app.add_handlers('.*$', [ + (r'%s/api/builder(/?)(.*)' % self.options['web_root'], webserve.ApiBuilder), + (r'%s/api(/?.*)' % self.options['web_root'], webapi.Api), + (r'%s/imagecache(/?.*)' % self.options['web_root'], webserve.CachedImages), + (r'%s/cache(/?.*)' % self.options['web_root'], webserve.Cache), + (r'%s/config/general(/?.*)' % self.options['web_root'], webserve.ConfigGeneral), + (r'%s/config/search(/?.*)' % self.options['web_root'], webserve.ConfigSearch), + (r'%s/config/providers(/?.*)' % self.options['web_root'], webserve.ConfigProviders), + (r'%s/config/subtitles(/?.*)' % self.options['web_root'], webserve.ConfigSubtitles), + (r'%s/config/postProcessing(/?.*)' % self.options['web_root'], webserve.ConfigPostProcessing), + (r'%s/config/notifications(/?.*)' % self.options['web_root'], webserve.ConfigNotifications), + (r'%s/config/anime(/?.*)' % self.options['web_root'], webserve.ConfigAnime), + (r'%s/config(/?.*)' % self.options['web_root'], webserve.Config), + (r'%s/errorlogs(/?.*)' % self.options['web_root'], webserve.ErrorLogs), + (r'%s/history(/?.*)' % self.options['web_root'], webserve.History), + (r'%s/home/is_alive(/?.*)' % self.options['web_root'], webserve.IsAliveHandler), + (r'%s/home/addShows(/?.*)' % self.options['web_root'], webserve.NewHomeAddShows), + (r'%s/home/postprocess(/?.*)' % self.options['web_root'], webserve.HomePostProcess), + (r'%s/home(/?.*)' % self.options['web_root'], webserve.Home), + (r'%s/manage/manageSearches(/?.*)' % self.options['web_root'], webserve.ManageSearches), + (r'%s/manage/showProcesses(/?.*)' % self.options['web_root'], webserve.showProcesses), + (r'%s/manage/(/?.*)' % self.options['web_root'], webserve.Manage), + (r'%s/ui(/?.*)' % self.options['web_root'], webserve.UI), + (r'%s/browser(/?.*)' % self.options['web_root'], webserve.WebFileBrowser), + (r'%s(/?.*)' % self.options['web_root'], webserve.MainHandler), + ]) + def run(self): - if self.enable_https: - protocol = 'https' - self.server = HTTPServer(self.app, ssl_options={'certfile': self.https_cert, 'keyfile': self.https_key}) - else: - protocol = 'http' - self.server = HTTPServer(self.app) + protocol, ssl_options = (('http', None), + ('https', {'certfile': self.https_cert, 'keyfile': self.https_key}))[self.enable_https] logger.log(u'Starting SickGear on ' + protocol + '://' + str(self.options['host']) + ':' + str( self.options['port']) + '/') try: - self.server.listen(self.options['port'], self.options['host']) - except: + self.server = self.app.listen(self.options['port'], self.options['host'], ssl_options=ssl_options, + xheaders=sickbeard.HANDLE_REVERSE_PROXY, protocol=protocol) + except (StandardError, Exception): etype, evalue, etb = sys.exc_info() logger.log( 'Could not start webserver on %s. Excpeption: %s, Error: %s' % (self.options['port'], etype, evalue), @@ -154,4 +149,4 @@ class WebServer(threading.Thread): def shutDown(self): self.alive = False - self.io_loop.stop() \ No newline at end of file + self.io_loop.stop() diff --git a/tests/common_tests.py b/tests/common_tests.py index d38f4aa4..0fdd987d 100644 --- a/tests/common_tests.py +++ b/tests/common_tests.py @@ -5,6 +5,7 @@ import os.path sys.path.insert(1, os.path.abspath('..')) from sickbeard import common +from sickbeard.name_parser.parser import NameParser class QualityTests(unittest.TestCase): @@ -14,6 +15,13 @@ class QualityTests(unittest.TestCase): second = common.Quality.nameQuality(fn) self.assertEqual(quality, second, 'fail %s != %s for case: %s' % (quality, second, fn)) + def check_proper_level(self, cases, is_anime=False): + np = NameParser(False, indexer_lookup=False, try_scene_exceptions=False, testing=True) + for case, level in cases: + p = np.parse(case) + second = common.Quality.get_proper_level(p.extra_info_no_name(), p.version, is_anime) + self.assertEqual(level, second, 'fail %s != %s for case: %s' % (level, second, case)) + # TODO: repack / proper ? air-by-date ? season rip? multi-ep? def test_SDTV(self): @@ -135,7 +143,8 @@ class QualityTests(unittest.TestCase): self.check_quality_names(common.Quality.FULLHDBLURAY, [ 'Test.Show.S01E02.1080p.BluRay.x264-GROUP', 'Test.Show.S01E02.1080p.HDDVD.x264-GROUP', - 'Test.Show.S01E02.1080p.Blu-ray.x264-GROUP']) + 'Test.Show.S01E02.1080p.Blu-ray.x264-GROUP', + 'Test Show S02 1080p Remux AVC FLAC 5.1']) def test_UHD4KWEB(self): self.check_quality_names(common.Quality.UHD4KWEB, [ @@ -158,6 +167,39 @@ class QualityTests(unittest.TestCase): self.check_quality_names(common.Quality.FULLHDBLURAY, ['Test Show - S01E02 - 1080p BluRay - GROUP']) self.check_quality_names(common.Quality.UNKNOWN, ['Test Show - S01E02 - Unknown - SiCKGEAR']) + def test_get_proper_level(self): + # release_name, expected level + self.check_proper_level([ + ('Test.Show.S01E13.PROPER.REPACK.720p.HDTV.x264-GROUP', 2), + ('Test.Show.S01E13.720p.WEBRip.AAC2.0.x264-GROUP', 0), + ('Test.Show.S01E13.PROPER.720p.HDTV.x264-GROUP', 1), + ('Test.Show.S03E09-E10.REAL.PROPER.720p.HDTV.x264-GROUP', 2), + ('Test.Show.S01E07.REAL.PROPER.1080p.WEB.x264-GROUP', 2), + ('Test.Show.S13E20.REAL.REPACK.720p.HDTV.x264-GROUP', 2), + ('Test.Show.S02E04.REAL.HDTV.x264-GROUP', 1), + ('Test.Show.S01E10.Episode.Name.HDTV.x264-GROUP', 0), + ('Test.Show.S12E10.1080p.WEB.x264-GROUP', 0), + ('Test.Show.S03E01.Real.720p.WEB-DL.DD5.1.H.264-GROUP', 1), + ('Test.Show.S04E06.REAL.PROPER.RERIP.720p.WEBRip.X264-GROUP', 2), + ('Test.Show.S01E09.REPACK.REAL.PROPER.HDTV.XviD-GROUP.[SOMETHING].GROUP', 3), + ('Test.Show.S01E13.REPACK.REAL.PROPER.720p.HDTV.x264-GROUP', 3), + ('Test.Show.S01E06.The.Episode.Name.PROPER.480p.BluRay.x264-GROUP', 1), + ('Test.Show.S01E19.PROPER.1080p.BluRay.x264-GROUP', 1), + ('Test.Show.S01E03.REAL.PROPER.720p.BluRay.x264-GROUP', 2), + ('Test.Show.S03E09.Episode.Name.720p.HDTV.x264-GROUP', 0), + ('Test.Show.S02E07.PROPER.HDTV.x264-GROUP', 1), + ('Test.Show.S02E12.REAL.REPACK.DSR.XviD-GROUP', 2), + ('Test.Show Part2.REAL.AC3.WS DVDRip XviD-GROUP', 1), + ('Test.Show.S01E02.Some.episode.title.REAL.READ.NFO.DVDRip.XviD-GROUP', 1) + ]) + + # TODO: add anime test cases + def test_get_proper_level_anime(self): + # release_name, expected level + self.check_proper_level([ + + ], is_anime=True) + if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(QualityTests) unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/tests/name_parser_tests.py b/tests/name_parser_tests.py index 28e35db9..894828a4 100644 --- a/tests/name_parser_tests.py +++ b/tests/name_parser_tests.py @@ -143,6 +143,19 @@ simple_test_cases = { parser.ParseResult(None, 'Show Name', None, [], 'WEB-DL', None, datetime.date(2010, 11, 23)), }, + 'uk_date_format': { + 'Show.Name.23.11.2010.Source.Quality.Etc-Group': + parser.ParseResult(None, 'Show Name', None, [], 'Source.Quality.Etc', 'Group', datetime.date(2010, 11, 23)), + 'Show Name - 23.11.2010': parser.ParseResult(None, 'Show Name', air_date=datetime.date(2010, 11, 23)), + 'Show.Name.11.23.2010.Source.Quality.Etc-Group': + parser.ParseResult(None, 'Show Name', None, [], 'Source.Quality.Etc', 'Group', datetime.date(2010, 11, 23)), + 'Show Name - 23-11-2010 - Ep Name': + parser.ParseResult(None, 'Show Name', extra_info='Ep Name', air_date=datetime.date(2010, 11, 23)), + '23-11-2010 - Ep Name': parser.ParseResult(None, extra_info='Ep Name', air_date=datetime.date(2010, 11, 23)), + 'Show.Name.23.11.2010.WEB-DL': + parser.ParseResult(None, 'Show Name', None, [], 'WEB-DL', None, datetime.date(2010, 11, 23)), + }, + 'anime_ultimate': { '[Tsuki] Bleach - 301 [1280x720][61D1D4EE]': parser.ParseResult(None, 'Bleach', None, [], '1280x720', 'Tsuki', None, [301]), @@ -308,10 +321,16 @@ combination_test_cases = [ unicode_test_cases = [ (u'The.Big.Bang.Theory.2x07.The.Panty.Pi\xf1ata.Polarization.720p.HDTV.x264.AC3-SHELDON.mkv', - parser.ParseResult(None, 'The.Big.Bang.Theory', 2, [7], '720p.HDTV.x264.AC3', 'SHELDON') + parser.ParseResult( + u'The.Big.Bang.Theory.2x07.The.Panty.Pi\xf1ata.Polarization.720p.HDTV.x264.AC3-SHELDON.mkv', + u'The Big Bang Theory', 2, [7], u'The.Panty.Pi\xf1ata.Polarization.720p.HDTV.x264.AC3', u'SHELDON', + version=-1) ), ('The.Big.Bang.Theory.2x07.The.Panty.Pi\xc3\xb1ata.Polarization.720p.HDTV.x264.AC3-SHELDON.mkv', - parser.ParseResult(None, 'The.Big.Bang.Theory', 2, [7], '720p.HDTV.x264.AC3', 'SHELDON') + parser.ParseResult( + u'The.Big.Bang.Theory.2x07.The.Panty.Pi\xf1ata.Polarization.720p.HDTV.x264.AC3-SHELDON.mkv', + u'The Big Bang Theory', 2, [7], u'The.Panty.Pi\xf1ata.Polarization.720p.HDTV.x264.AC3', u'SHELDON', + version=-1) ), ] @@ -319,14 +338,11 @@ failure_cases = ['7sins-jfcs01e09-720p-bluray-x264'] class UnicodeTests(test.SickbeardTestDBCase): - @staticmethod - def _test_unicode(name, result): - np = parser.NameParser(True) - try: - parse_result = np.parse(name) - except parser.InvalidShowException: - return False + def _test_unicode(self, name, result): + result.which_regex = ['fov'] + parse_result = parser.NameParser(True, testing=True).parse(name) + self.assertEqual(parse_result, result) # this shouldn't raise an exception void = repr(str(parse_result)) @@ -460,6 +476,10 @@ class BasicTests(test.SickbeardTestDBCase): np = parser.NameParser(False, testing=True) self._test_names(np, 'scene_date_format') + def test_uk_date_format_names(self): + np = parser.NameParser(False, testing=True) + self._test_names(np, 'uk_date_format') + def test_standard_file_names(self): np = parser.NameParser(testing=True) self._test_names(np, 'standard', lambda x: x + '.avi') diff --git a/tests/tv_tests.py b/tests/tv_tests.py index 2ebdb3f3..4edb8dd8 100644 --- a/tests/tv_tests.py +++ b/tests/tv_tests.py @@ -98,6 +98,34 @@ class TVTests(test.SickbeardTestDBCase): #TODO: implement +class TVFormatPatternTests(test.SickbeardTestDBCase): + + def setUp(self): + super(TVFormatPatternTests, self).setUp() + sickbeard.showList = [] + + def test_getEpisode(self): + show = TVShow(1, 1, 'en') + show.name = 'show name' + show.tvrname = 'show name' + show.network = 'cbs' + show.genre = 'crime' + show.runtime = 40 + show.status = '5' + show.airs = 'monday' + show.startyear = 1987 + sickbeard.showList = [show] + show.episodes[1] = {} + show.episodes[1][1] = TVEpisode(show, 1, 1, '16)') + show.episodes[1][1].dirty = False + show.episodes[1][1].name = None + self.assertEqual(show.episodes[1][1].dirty, False) + self.assertEqual(show.episodes[1][1]._format_pattern('%SN - %Sx%0E - %EN - %QN'), 'show name - 1x01 - - Unknown') + show.episodes[1][1].dirty = False + show.episodes[1][1].name = 'ep name' + self.assertEqual(show.episodes[1][1].dirty, True) + self.assertEqual(show.episodes[1][1]._format_pattern('%SN - %Sx%0E - %EN - %QN'), 'show name - 1x01 - ep name - Unknown') + if __name__ == '__main__': print('==================') print('STARTING - TV TESTS') @@ -111,3 +139,6 @@ if __name__ == '__main__': print('######################################################################') suite = unittest.TestLoader().loadTestsFromTestCase(TVTests) unittest.TextTestRunner(verbosity=2).run(suite) + print('######################################################################') + suite = unittest.TestLoader().loadTestsFromTestCase(TVFormatPatternTests) + unittest.TextTestRunner(verbosity=2).run(suite)
    Master (changes)(most stable)Master (changes)(most stable)
    Development (changes)(mostly stable)Development (changes)(mostly stable)
    [ ++ Install Guides ++ ][ Frequently Answered ][ Discover Wiki ]