def notify_login(self, ipaddress=''): ''' Send a notification that SickChill was logged into remotely ipaddress: The ip SickChill was logged into from ''' if settings.USE_EMAIL: to = self._generate_recipients(None) if not len(to): logger.debug( 'Skipping email notify because there are no configured recipients' ) else: try: msg = MIMEMultipart('alternative') msg.attach( MIMEText('SickChill Notification - Remote Login\n' 'New login from IP: {0}\n\n' 'Powered by SickChill.'.format(ipaddress))) msg.attach( MIMEText( '<body style="font-family:Helvetica, Arial, sans-serif;">' '<h3>SickChill Notification - Remote Login</h3><br>' '<p>New login from IP: <a href="http://geomaplookup.net/?ip={0}">{0}</a>.<br><br>' '<footer style="margin-top: 2.5em; padding: .7em 0; ' 'color: #777; border-top: #BBB solid 1px;">' 'Powered by SickChill.</footer></body>'.format( ipaddress), 'html')) except Exception: try: msg = MIMEText(ipaddress) except Exception: msg = MIMEText('SickChill Remote Login') msg['Subject'] = 'New Login from IP: {0}'.format(ipaddress) msg['From'] = settings.EMAIL_FROM msg['To'] = ','.join(to) msg['Date'] = formatdate(localtime=True) if self._sendmail(settings.EMAIL_HOST, settings.EMAIL_PORT, settings.EMAIL_FROM, settings.EMAIL_TLS, settings.EMAIL_USER, settings.EMAIL_PASSWORD, to, msg): logger.debug('Login notification sent to [{0}]'.format(to)) else: logger.warning('Login notification error: {0}'.format( self.last_err))
def _add_torrent_file(self, result): if not (self.auth and result): return False try: # Send torrent file with params to rTorrent and optionally start download torrent = self.auth.load_torrent(result.content, start=not settings.TORRENT_PAUSED, params=self._get_params(result)) if not torrent: return False return True except Exception as error: logger.warning(_(f'Error while sending torrent: {error}')) return False
def popularShows(self): """ Fetches data from IMDB to show a list of popular shows. """ t = PageTemplate(rh=self, filename="addShows_popularShows.mako") try: popular_shows = imdb_popular.fetch_popular_shows() imdb_exception = None except Exception as error: logger.warning("Could not get popular shows: {0}".format(str(error))) logger.debug(traceback.format_exc()) popular_shows = None imdb_exception = error return t.render(title=_("Popular Shows"), header=_("Popular Shows"), popular_shows=popular_shows, imdb_exception=imdb_exception, topmenu="home", controller="addShows", action="popularShows")
def _api_call(self, apikey, params=None, results_per_page=1000, offset=0): server = jsonrpclib.Server(self.urls["base_url"]) parsed_json = {} try: parsed_json = server.getTorrents(apikey, params or {}, int(results_per_page), int(offset)) time.sleep(cpu_presets[settings.CPU_PRESET]) except jsonrpclib.jsonrpc.ProtocolError as error: if error == (-32001, "Invalid API Key"): logger.warning( "The API key you provided was rejected because it is invalid. Check your provider configuration." ) elif error == (-32002, "Call Limit Exceeded"): logger.warning( "You have exceeded the limit of 150 calls per hour, per API key which is unique to your user account" ) else: logger.exception( "JSON-RPC protocol error while accessing provider. Error: {0} " .format(repr(error))) parsed_json = {"api-error": str(error)} return parsed_json except socket.timeout: logger.warning("Timeout while accessing provider") except socket.error as error: # Note that sometimes timeouts are thrown as socket errors logger.warning( "Socket error while accessing provider. Error: {0} ".format( error[1])) except Exception as error: errorstring = str(error) if errorstring.startswith("<") and errorstring.endswith(">"): errorstring = errorstring[1:-1] logger.warning( "Unknown error while accessing provider. Error: {0} ".format( errorstring)) return parsed_json
def get_newznab_categories(self, just_caps=False): """ Uses the newznab provider url and apikey to get the capabilities. Makes use of the default newznab caps param. e.a. http://yournewznab/api?t=caps&apikey=skdfiw7823sdkdsfjsfk Returns a tuple with (succes or not, array with dicts [{'id': '5070', 'name': 'Anime'}, {'id': '5080', 'name': 'Documentary'}, {'id': '5020', 'name': 'Foreign'}...etc}], error message) """ return_categories = [] if not self._check_auth(): return False, return_categories, 'Provider requires auth and your key is not set' url_params = {'t': 'caps'} if self.needs_auth and self.key: url_params['apikey'] = self.key data = self.get_url(urljoin(self.url, 'api'), params=url_params, returns='text') if not data: error_string = 'Error getting caps xml for [{0}]'.format(self.name) logger.warning(error_string) return False, return_categories, error_string with BS4Parser(data, 'html5lib') as html: try: self.torznab = html.find('server').get('title') == 'Jackett' except AttributeError: self.torznab = False if not html.find('categories'): error_string = 'Error parsing caps xml for [{0}]'.format(self.name) logger.debug(error_string) return False, return_categories, error_string self.caps = html.find('searching') if just_caps: return True, return_categories, 'Just checking caps!' for category in html('category'): if 'TV' in category.get('name', '') and category.get('id', ''): return_categories.append({'id': category['id'], 'name': category['name']}) for subcat in category('subcat'): if subcat.get('name', '') and subcat.get('id', ''): return_categories.append({'id': subcat['id'], 'name': subcat['name']}) return True, return_categories, ''
def check_for_new_news(self): """ Checks GitHub for the latest news. returns: str, a copy of the news force: ignored """ # Grab a copy of the news logger.debug('check_for_new_news: Checking GitHub for latest news.') try: news = helpers.getURL(settings.NEWS_URL, session=self.session, returns='text') except Exception: logger.warning( 'check_for_new_news: Could not load news from repo.') news = '' if not news: return '' try: last_read = datetime.datetime.strptime(settings.NEWS_LAST_READ, '%Y-%m-%d') except Exception: last_read = 0 settings.NEWS_UNREAD = 0 found_news = False for match in re.finditer(r'^####\s*(\d{4}-\d{2}-\d{2})\s*####', news, re.M): if not found_news: found_news = True settings.NEWS_LATEST = match.group(1) try: if datetime.datetime.strptime(match.group(1), '%Y-%m-%d') > last_read: settings.NEWS_UNREAD += 1 except Exception: pass return news
def login(self): if self.token and self.token_expires and datetime.datetime.now() < self.token_expires: return True login_params = { "get_token": "get_token", "format": "json", "app_id": "sickchill" } response = self.get_url(self.urls["api"], params=login_params, returns="json") if not response: logger.warning("Unable to connect to provider") return False self.token = response.get("token") self.token_expires = datetime.datetime.now() + datetime.timedelta(minutes=14) if self.token else None return self.token is not None
def update_urls(self, new_url, custom=False): if custom and not new_url: return True if not validators.url(new_url): if custom: logger.warning("Invalid custom url: {0}".format(self.custom_url)) else: logger.debug('Url changing has failed!') return False self.url = new_url self.urls = { 'login': urljoin(self.url, 'user/login'), 'search': urljoin(self.url, 'engine/search') } return True
def get(self): if self.get_query_argument( 'u', None) == settings.WEB_USERNAME and self.get_query_argument( 'p', None) == settings.WEB_PASSWORD: if not len(settings.API_KEY or ''): settings.API_KEY = helpers.generateApiKey() result = {'success': True, 'api_key': settings.API_KEY} else: result = { 'success': False, 'error': _('Failed authentication while getting api key') } logger.warning( _('Authentication failed during api key request: {traceback}'. format(traceback=traceback.format_exc()))) return self.finish(result)
def _check_auth_from_data(self, parsed_data, is_XML=True): if not parsed_data: return self._check_auth() if is_XML: # provider doesn't return xml on error return True if 'notice' in parsed_data: description_text = parsed_data.get('notice') if 'information is incorrect' in description_text: logger.warning('Invalid api key. Check your settings') elif '0 results matched your terms' not in description_text: logger.debug('Unknown error: {0}'.format(description_text)) return False return True
def login(self): login_params = { 'nev': self.username, 'pass': self.password, 'submitted': '1', } response = self.get_url(self.urls["login"], post_data=login_params, returns="text") if not response: logger.warning("Unable to connect to provider") return False if re.search('images/warning.png', response): logger.warning("Invalid username or password. Check your settings") return False return True
def login(self): if any(dict_from_cookiejar(self.session.cookies).values()): return True login_params = {"username": self.username, "password": self.password, "login": "******"} response = self.get_url(self.urls["login"], post_data=login_params, returns="text") if not response: logger.warning("Unable to connect to provider") return False if re.search("Username or password incorrect", response): logger.warning("Invalid username or password. Check your settings") return False return True
def need_update(self): # need this to run first to set self._newest_commit_hash try: self._check_github_for_update() except Exception as e: logger.warning( "Unable to contact github, can't check for update: " + repr(e)) return False if self.branch != self._find_installed_branch(): logger.debug("Branch checkout: " + self._find_installed_branch() + "->" + self.branch) return True if not self._cur_commit_hash or self._num_commits_behind > 0: return True return False
def login(self): if any(dict_from_cookiejar(self.session.cookies).values()): return True login_params = {'username': self.username, 'password': self.password, 'login_pin': self.pin} response = self.get_url(self.urls['login'], post_data=login_params, returns='text') if not response: logger.warning("Unable to connect to provider") return False if re.search('Username or password incorrect', response): logger.warning("Invalid username or password. Check your settings") return False return True
def delete_folder(folder, check_empty=True): """ Removes a folder from the filesystem :param folder: Path to folder to remove :param check_empty: Boolean, check if the folder is empty before removing it, defaults to True :return: True on success, False on failure """ # check if it's a folder if not os.path.isdir(folder): return False # check if it isn't TV_DOWNLOAD_DIR if settings.TV_DOWNLOAD_DIR and helpers.real_path( folder) == helpers.real_path(settings.TV_DOWNLOAD_DIR): return False # check if it's empty folder when wanted checked if check_empty: check_files = os.listdir(folder) if check_files: logger.info( "Not deleting folder {0} found the following files: {1}". format(folder, check_files)) return False try: logger.info("Deleting folder (if it's empty): {0}".format(folder)) os.rmdir(folder) except (OSError, IOError) as e: logger.warning("Warning: unable to delete folder: {0}: {1}".format( folder, str(e))) return False else: try: logger.info("Deleting folder: " + folder) shutil.rmtree(folder) except (OSError, IOError) as e: logger.warning("Warning: unable to delete folder: {0}: {1}".format( folder, str(e))) return False return True
def addShowToTraktWatchList(self): if settings.TRAKT_SYNC_WATCHLIST and settings.USE_TRAKT: logger.debug( "SHOW_WATCHLIST::ADD::START - Look for Shows to Add to Trakt Watchlist" ) if settings.showList is not None: trakt_data = [] for show in settings.showList: if not self._checkInList(show.idxr.slug, str(show.indexerid), "0", "0", List="Show"): logger.debug( "Adding Show: Indexer {0} {1} - {2} to Watchlist". format(show.idxr.name, str(show.indexerid), show.name)) show_el = { "title": show.name, "year": show.startyear, "ids": { show.idxr.slug: show.indexerid } } trakt_data.append(show_el) if trakt_data: try: data = {"shows": trakt_data} self.trakt_api.traktRequest("sync/watchlist", data, method="POST") self._getShowWatchlist() except traktException as e: logger.warning( "Could not connect to Trakt service. Error: {0}". format(str(e))) logger.debug( "SHOW_WATCHLIST::ADD::FINISH - Look for Shows to Add to Trakt Watchlist" )
def run(self): super(QueueItemRemove, self).run() logger.info('Removing {0}'.format(self.show.name)) self.show.deleteShow(full=self.full) if settings.USE_TRAKT: try: settings.traktCheckerScheduler.action.removeShowFromTraktLibrary( self.show) except Exception as error: logger.warning( _('Unable to delete show from Trakt: {0}. Error: {1}'). format(self.show.name, error)) # If any notification fails, don't stop removal try: # TODO: ep_obj is undefined here, so all of these will fail. # send notifications # notifiers.notify_download(ep_obj._format_pattern('%SN - %Sx%0E - %EN - %QN')) # do the library update for KODI notifiers.kodi_notifier.update_library(self.show.name) # do the library update for Plex notifiers.plex_notifier.update_library(self.show) # do the library update for EMBY notifiers.emby_notifier.update_library(self.show) # 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.addFolder(self.show._location) # do the library update for pyTivo notifiers.pytivo_notifier.update_library(self.show) except Exception: logger.info( _("Some notifications could not be sent. Continuing removal of {}..." ).format(self.show.name)) super(QueueItemRemove, self).finish() self.finish()
def getTrendingShows(self, traktList=None): """ Display the new show page which collects a tvdb id, folder, and extra options and posts them to addNewShow """ t = PageTemplate(rh=self, filename="trendingShows.mako") if not traktList: traktList = "" traktList = traktList.lower() if traktList == "trending": page_url = "shows/trending" elif traktList == "popular": page_url = "shows/popular" elif traktList == "anticipated": page_url = "shows/anticipated" elif traktList == "collected": page_url = "shows/collected" elif traktList == "watched": page_url = "shows/watched" elif traktList == "played": page_url = "shows/played" elif traktList == "recommended": page_url = "recommendations/shows" elif traktList == "newshow": page_url = "calendars/all/shows/new/{0}/30".format( datetime.date.today().strftime("%Y-%m-%d")) elif traktList == "newseason": page_url = "calendars/all/shows/premieres/{0}/30".format( datetime.date.today().strftime("%Y-%m-%d")) else: page_url = "shows/anticipated" trending_shows = [] black_list = False try: trending_shows, black_list = trakt_trending.fetch_trending_shows( traktList, page_url) except Exception as e: logger.warning("Could not get trending shows: {0}".format(str(e))) return t.render(black_list=black_list, trending_shows=trending_shows)
def download_result(self, result): if not self.login(): return False urls, filename = self._make_url(result) for url in urls: if 'NO_DOWNLOAD_NAME' in url: continue if isinstance(url, tuple): referer = url[1] url = url[0] else: referer = '/'.join(url.split('/')[:3]) + '/' if url.startswith('http'): self.headers.update({'Referer': referer}) logger.info('Downloading a result from {0} at {1}'.format( self.name, url)) downloaded_filename = download_file( url, filename, session=self.session, headers=self.headers, hooks={'response': self.get_url_hook}, return_filename=True) if downloaded_filename: if self._verify_download(downloaded_filename): logger.info( 'Saved result to {0}'.format(downloaded_filename)) return True logger.warning('Could not download {0}'.format(url)) remove_file_failed(downloaded_filename) if urls: logger.warning('Failed to download any results') return False
def _sendRegistration(self, host=None, password=None, name='SickChill Notification'): opts = {} if host is None: hostParts = settings.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 is None: opts['password'] = settings.GROWL_PASSWORD else: opts['password'] = password opts['app'] = 'SickChill' opts['debug'] = False # Send Registration register = gntp.core.GNTPRegister() register.add_header('Application-Name', opts['app']) register.add_header('Application-Icon', settings.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) 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.warning("GROWL: Unable to send growl to " + opts['host'] + ":" + str(opts['port']) + " - " + str(e)) return False
def download_result(self, result): if not self.login(): return False urls, filename = self._make_url(result) for url in urls: if "NO_DOWNLOAD_NAME" in url: continue if isinstance(url, tuple): referer = url[1] url = url[0] else: referer = "/".join(url.split("/")[:3]) + "/" if url.startswith("http"): self.headers.update({"Referer": referer}) logger.info("Downloading a result from {0} at {1}".format( self.name, url)) downloaded_filename = download_file( url, filename, session=self.session, headers=self.headers, hooks={"response": self.get_url_hook}, return_filename=True) if downloaded_filename: if self._verify_download(downloaded_filename): logger.info( "Saved result to {0}".format(downloaded_filename)) return True logger.warning("Could not download {0}".format(url)) remove_file_failed(downloaded_filename) if urls: logger.warning("Failed to download any results") return False
def _sendmail(self, host, port, smtp_from, use_tls, user, pwd, to, msg, smtpDebug=False): logger.debug( 'HOST: {0}; PORT: {1}; FROM: {2}, TLS: {3}, USER: {4}, PWD: {5}, TO: {6}' .format(host, port, smtp_from, use_tls, user, pwd, to)) try: srv = smtplib.SMTP(host, int(port)) except Exception as e: logger.warning('Exception generated while sending e-mail: ' + str(e)) # logger.debug(traceback.format_exc()) self.last_err = '{0}'.format(e) return False if smtpDebug: srv.set_debuglevel(1) try: if use_tls in ('1', True) or (user and pwd): logger.debug('Sending initial EHLO command!') srv.ehlo() if use_tls in ('1', True): logger.debug('Sending STARTTLS command!') srv.starttls() srv.ehlo() if user and pwd: logger.debug('Sending LOGIN command!') srv.login(user, pwd) srv.sendmail(smtp_from, to, msg.as_string()) srv.quit() return True except Exception as e: self.last_err = '{0}'.format(e) return False
def post(self, next_=None): notifiers.notify_login(self.request.remote_ip) if self.get_body_argument( 'username', None) == settings.WEB_USERNAME and self.get_body_argument( 'password', None) == settings.WEB_PASSWORD: remember_me = config.checkbox_to_value( self.get_body_argument('remember_me', '0')) self.set_secure_cookie('sickchill_user', settings.API_KEY, expires_days=(None, 30)[remember_me]) logger.info('User logged into the SickChill web interface') else: logger.warning( 'User attempted a failed login to the SickChill web interface from IP: ' + self.request.remote_ip) next_ = self.get_query_argument('next', next_) self.redirect(next_ or '/' + settings.DEFAULT_PAGE + '/')
def subtitles_enabled(video): """ Parse video filename to a show to check if it has subtitle enabled :param video: video filename to be parsed """ try: parse_result = NameParser().parse(video, cache_result=True) except (InvalidNameException, InvalidShowException): logger.warning('Not enough information to parse filename into a valid show. Consider add scene exceptions or improve naming for: {0}'.format(video)) return False if parse_result.show.indexerid: main_db_con = db.DBConnection() sql_results = main_db_con.select("SELECT subtitles FROM tv_shows WHERE indexer_id = ? LIMIT 1", [parse_result.show.indexerid]) return bool(sql_results[0]["subtitles"]) if sql_results else False else: logger.warning('Empty indexer ID for: {0}'.format(video)) return False
def __init__(self, filename="sickchill.db", suffix=None, row_type=None): self.filename = filename self.suffix = suffix self.row_type = row_type self.full_path = db_full_path(self.filename, self.suffix) if filename == "sickchill.db" and not os.path.isfile(self.full_path): sickbeard_db = db_full_path("sickbeard.db", suffix) if os.path.isfile(sickbeard_db): os.rename(sickbeard_db, self.full_path) try: if self.filename not in db_cons or not db_cons[self.filename]: db_locks[self.filename] = threading.Lock() self.connection = sqlite3.connect(self.full_path, 20, check_same_thread=False) db_cons[self.filename] = self.connection else: self.connection = db_cons[self.filename] # start off row factory configured as before out of # paranoia but wait to do so until other potential users # of the shared connection are done using # it... technically not required as row factory is reset # in all the public methods after the lock has been # acquired with db_locks[self.filename]: self._set_row_factory() except OperationalError: # noinspection PyUnresolvedReferences logger.warning( _("Please check your database owner/permissions: {db_filename}" ).format(db_filename=self.full_path)) except Exception as e: self._error_log_helper(e, logger.ERROR, locals(), None, "DBConnection.__init__") raise
def convert_archived_to_compound(self): logger.debug(_("Checking for archived episodes not qualified")) sql_results = self.connection.select( "SELECT episode_id, showid, status, location, season, episode FROM tv_episodes WHERE status = ?", [common.ARCHIVED]) if sql_results: logger.warning( _("Found {count} shows with bare archived status, attempting automatic conversion..." .format(count=len(sql_results)))) for archivedEp in sql_results: fixed_status = common.Quality.compositeStatus( common.ARCHIVED, common.Quality.UNKNOWN) existing = archivedEp["location"] and os.path.exists( archivedEp["location"]) if existing: quality = common.Quality.nameQuality(archivedEp["location"]) fixed_status = common.Quality.compositeStatus( common.ARCHIVED, quality) old_status = common.statusStrings[common.ARCHIVED] new_status = common.statusStrings[fixed_status] archived_episode = archivedEp["showid"] ep = episode_num(archivedEp["season"]) episode_id = archivedEp["episode_id"] location = archivedEp["location"] or "unknown location" result = ("NOT FOUND", "EXISTS")[bool(existing)] logger.info( _("Changing status from {old_status} to {new_status} for {archived_episode}: {ep} at {location} (File {result})" .format(old_status=old_status, new_status=new_status, archived_episode=archived_episode, ep=ep, location=location, result=result))) self.connection.action( "UPDATE tv_episodes SET status = ? WHERE episode_id = ?", [fixed_status, episode_id])
def _notify_emby(self, message, host=None, emby_apikey=None): """Handles notifying Emby host via HTTP API Returns: Returns True for no issue or False if there was an error """ url = urljoin(host or settings.EMBY_HOST, 'emby/Notifications/Admin') params = {'Name': 'SickChill', 'Description': message, 'ImageUrl': settings.LOGO_URL} try: session = self.__make_session(emby_apikey) response = session.get(url, params=params) if response: logger.debug("EMBY: HTTP response: {0}".format(response.text.replace('\n', ''))) response.raise_for_status() return True except requests.exceptions.RequestException as error: logger.warning(f"EMBY: Warning: Could not contact Emby at {url} {error}") return False
def _add_torrent_uri(self, result): if not (self.auth and result): return False try: # Send torrent magnet with params to rTorrent and optionally start download torrent = self.auth.load_magnet(result.url, result.hash, start=not settings.TORRENT_PAUSED, params=self._get_params(result)) if not torrent: return False return True except Exception as error: logger.warning( _('Error while sending torrent: {error}'.format(error=error))) return False
def findShow(self, indexer, indexerid): traktShow = None try: library = self.trakt_api.traktRequest( "sync/collection/shows") or [] if not library: logger.debug( "No shows found in your library, aborting library update") return traktShow = [ x for x in library if int(indexerid) == int( x["show"]["ids"][sickchill.indexer.slug(indexer)] or -1) ] except traktException as e: logger.warning( "Could not connect to Trakt service. Aborting library check. Error: {0}" .format(repr(e))) return traktShow
def login(self): login_params = { "id": self.username, "pass": self.password, } self.update_urls(self.custom_url, True) response = self.get_url(self.urls["login"], post_data=login_params, returns="response") if response and self.url not in response.url: new_url = response.url.split("user/login")[0] logger.debug("Changing base url from {} to {}".format(self.url, new_url)) if not self.update_urls(new_url): return False response = self.get_url(self.urls["login"], post_data=login_params, returns="response") # The login is now an AJAX call (401 : Bad credentials, 200 : Logged in, other : server failure) if not response or response.status_code != 200: logger.warning("Unable to connect to provider") return False else: # It seems we are logged, let's verify that ! response = self.get_url(self.url, returns="response") if response.status_code != 200: logger.warning("Unable to connect to provider") return False if "logout" not in response.text: logger.warning("Invalid username or password. Check your settings") return False return True