Select Git revision
SickBeard.py 21.84 KiB
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*
# Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of SickRage.
#
# SickRage 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.
#
# SickRage 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 SickRage. If not, see <http://www.gnu.org/licenses/>.
"""
Usage: SickBeard.py [OPTION]...
Options:
-h, --help Prints this message
-q, --quiet Disables logging to console
--nolaunch Suppress launching web browser on startup
-d, --daemon Run as double forked daemon (with --quiet --nolaunch)
On Windows and MAC, this option is ignored but still
applies --quiet --nolaunch
--pidfile=[FILE] Combined with --daemon creates a pid file
-p, --port=[PORT] Override default/configured port to listen on
--datadir=[PATH] Override folder (full path) as location for
storing database, config file, cache, and log files
Default SickRage directory
--config=[FILE] Override config filename for loading configuration
Default config.ini in SickRage directory or
location specified with --datadir
--noresize Prevent resizing of the banner/posters even if PIL
is installed
"""
from __future__ import unicode_literals
from __future__ import print_function
import codecs
import datetime
import getopt
import io
import locale
import os
import shutil
import signal
import subprocess
import sys
import threading
import time
import traceback
codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
if (2, 7, 99) < sys.version_info < (2, 7):
print('Sorry, requires Python 2.7')
sys.exit(1)
# https://mail.python.org/pipermail/python-dev/2014-September/136300.html
if sys.version_info >= (2, 7, 9):
import ssl
ssl._create_default_https_context = ssl._create_unverified_context # pylint: disable=protected-access
import shutil_custom # pylint: disable=import-error
shutil.copyfile = shutil_custom.copyfile_custom
# Fix mimetypes on misconfigured systems
import mimetypes
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("application/sfont", ".otf")
mimetypes.add_type("application/sfont", ".ttf")
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("application/font-woff", ".woff")
# Not sure about this one, but we also have halflings in .woff so I think it wont matter
# mimetypes.add_type("application/font-woff2", ".woff2")
# Do this before importing sickbeard, to prevent locked files and incorrect import
OLD_TORNADO = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tornado'))
if os.path.isdir(OLD_TORNADO):
shutil.move(OLD_TORNADO, OLD_TORNADO + '_kill')
shutil.rmtree(OLD_TORNADO + '_kill')
import sickbeard
from sickbeard import db, logger, network_timezones, failed_history, name_cache
from sickbeard.tv import TVShow
from sickbeard.webserveInit import SRWebServer
from sickbeard.event_queue import Events
from configobj import ConfigObj # pylint: disable=import-error
from sickrage.helper.encoding import ek
# http://bugs.python.org/issue7980#msg221094
THROWAWAY = datetime.datetime.strptime('20110101', '%Y%m%d')
signal.signal(signal.SIGINT, sickbeard.sig_handler)
signal.signal(signal.SIGTERM, sickbeard.sig_handler)
class SickRage(object):
# pylint: disable=too-many-instance-attributes
"""
Main SickRage module
"""
def __init__(self):
# system event callback for shutdown/restart
sickbeard.events = Events(self.shutdown)
# daemon constants
self.run_as_daemon = False
self.create_pid = False
self.pid_file = ''
# web server constants
self.web_server = None
self.forced_port = None
self.no_launch = False
self.web_host = '0.0.0.0'
self.start_port = sickbeard.WEB_PORT
self.web_options = {}
self.log_dir = None
self.console_logging = True
@staticmethod
def clear_cache():
"""
Remove the Mako cache directory
"""
try:
cache_folder = ek(os.path.join, sickbeard.CACHE_DIR, 'mako')
if os.path.isdir(cache_folder):
shutil.rmtree(cache_folder)
except Exception: # pylint: disable=broad-except
logger.log('Unable to remove the cache/mako directory!', logger.WARNING)
@staticmethod
def help_message():
"""
Print help message for commandline options
"""
help_msg = __doc__
help_msg = help_msg.replace('SickBeard.py', sickbeard.MY_FULLNAME)
help_msg = help_msg.replace('SickRage directory', sickbeard.PROG_DIR)
return help_msg
def start(self): # pylint: disable=too-many-branches,too-many-statements
"""
Start SickRage
"""
# do some preliminary stuff
sickbeard.MY_FULLNAME = ek(os.path.normpath, ek(os.path.abspath, __file__))
sickbeard.MY_NAME = ek(os.path.basename, sickbeard.MY_FULLNAME)
sickbeard.PROG_DIR = ek(os.path.dirname, sickbeard.MY_FULLNAME)
sickbeard.DATA_DIR = sickbeard.PROG_DIR
sickbeard.MY_ARGS = sys.argv[1:]
try:
locale.setlocale(locale.LC_ALL, '')
sickbeard.SYS_ENCODING = locale.getpreferredencoding()
except (locale.Error, IOError):
sickbeard.SYS_ENCODING = 'UTF-8'
# pylint: disable=no-member
if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING.lower() in ('ansi_x3.4-1968', 'us-ascii', 'ascii', 'charmap') or \
(sys.platform.startswith('win') and sys.getwindowsversion()[0] >= 6 and str(getattr(sys.stdout, 'device', sys.stdout).encoding).lower() in ('cp65001', 'charmap')):
sickbeard.SYS_ENCODING = 'UTF-8'
# TODO: Continue working on making this unnecessary, this hack creates all sorts of hellish problems
if not hasattr(sys, 'setdefaultencoding'):
reload(sys)
try:
# On non-unicode builds this will raise an AttributeError, if encoding type is not valid it throws a LookupError
sys.setdefaultencoding(sickbeard.SYS_ENCODING) # pylint: disable=no-member
except (AttributeError, LookupError):
sys.exit('Sorry, you MUST add the SickRage folder to the PYTHONPATH environment variable\n'
'or find another way to force Python to use %s for string encoding.' % sickbeard.SYS_ENCODING)
# Need console logging for SickBeard.py and SickBeard-console.exe
self.console_logging = (not hasattr(sys, 'frozen')) or (sickbeard.MY_NAME.lower().find('-console') > 0)
# Rename the main thread
threading.currentThread().name = 'MAIN'
try:
opts, args_ = getopt.getopt(
sys.argv[1:], 'hqdp::',
['help', 'quiet', 'nolaunch', 'daemon', 'pidfile=', 'port=', 'datadir=', 'config=', 'noresize']
)
except getopt.GetoptError:
sys.exit(self.help_message())
for option, value in opts:
# Prints help message
if option in ('-h', '--help'):
sys.exit(self.help_message())
# For now we'll just silence the logging
if option in ('-q', '--quiet'):
self.console_logging = False
# Suppress launching web browser
# Needed for OSes without default browser assigned
# Prevent duplicate browser window when restarting in the app
if option in ('--nolaunch',):
self.no_launch = True
# Override default/configured port
if option in ('-p', '--port'):
try:
self.forced_port = int(value)
except ValueError:
sys.exit('Port: {0} is not a number. Exiting.'.format(value))
# Run as a double forked daemon
if option in ('-d', '--daemon'):
self.run_as_daemon = True
# When running as daemon disable console_logging and don't start browser
self.console_logging = False
self.no_launch = True
if sys.platform == 'win32' or sys.platform == 'darwin':
self.run_as_daemon = False
# Write a pid file if requested
if option in ('--pidfile',):
self.create_pid = True
self.pid_file = str(value)
# If the pid file already exists, SickRage may still be running, so exit
if ek(os.path.exists, self.pid_file):
sys.exit('PID file: {0} already exists. Exiting.'.format(self.pid_file))
# Specify folder to load the config file from
if option in ('--config',):
sickbeard.CONFIG_FILE = ek(os.path.abspath, value)
# Specify folder to use as the data directory
if option in ('--datadir',):
sickbeard.DATA_DIR = ek(os.path.abspath, value)
# Prevent resizing of the banner/posters even if PIL is installed
if option in ('--noresize',):
sickbeard.NO_RESIZE = True
# The pid file is only useful in daemon mode, make sure we can write the file properly
if self.create_pid:
if self.run_as_daemon:
pid_dir = ek(os.path.dirname, self.pid_file)
if not ek(os.access, pid_dir, os.F_OK):
sys.exit('PID dir: {0} doesn\'t exist. Exiting.'.format(pid_dir))
if not ek(os.access, pid_dir, os.W_OK):
sys.exit('PID dir: {0} must be writable (write permissions). Exiting.'.format(pid_dir))
else:
if self.console_logging:
sys.stdout.write('Not running in daemon mode. PID file creation disabled.\n')
self.create_pid = False
# If they don't specify a config file then put it in the data dir
if not sickbeard.CONFIG_FILE:
sickbeard.CONFIG_FILE = ek(os.path.join, sickbeard.DATA_DIR, 'config.ini')
# Make sure that we can create the data dir
if not ek(os.access, sickbeard.DATA_DIR, os.F_OK):
try:
ek(os.makedirs, sickbeard.DATA_DIR, 0o744)
except os.error:
raise SystemExit('Unable to create data directory: {0}'.format(sickbeard.DATA_DIR))
# Make sure we can write to the data dir
if not ek(os.access, sickbeard.DATA_DIR, os.W_OK):
raise SystemExit('Data directory must be writeable: {0}'.format(sickbeard.DATA_DIR))
# Make sure we can write to the config file
if not ek(os.access, sickbeard.CONFIG_FILE, os.W_OK):
if ek(os.path.isfile, sickbeard.CONFIG_FILE):
raise SystemExit('Config file must be writeable: {0}'.format(sickbeard.CONFIG_FILE))
elif not ek(os.access, ek(os.path.dirname, sickbeard.CONFIG_FILE), os.W_OK):
raise SystemExit('Config file root dir must be writeable: {0}'.format(ek(os.path.dirname, sickbeard.CONFIG_FILE)))
ek(os.chdir, sickbeard.DATA_DIR)
# Check if we need to perform a restore first
restore_dir = ek(os.path.join, sickbeard.DATA_DIR, 'restore')
if ek(os.path.exists, restore_dir):
success = self.restore_db(restore_dir, sickbeard.DATA_DIR)
if self.console_logging:
sys.stdout.write('Restore: restoring DB and config.ini {0}!\n'.format(('FAILED', 'SUCCESSFUL')[success]))
# Load the config and publish it to the sickbeard package
if self.console_logging and not ek(os.path.isfile, sickbeard.CONFIG_FILE):
sys.stdout.write('Unable to find {0}, all settings will be default!\n'.format(sickbeard.CONFIG_FILE))
sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE)
# Initialize the config and our threads
sickbeard.initialize(consoleLogging=self.console_logging)
if self.run_as_daemon:
self.daemonize()
# Get PID
sickbeard.PID = os.getpid()
# Build from the DB to start with
self.load_shows_from_db()
logger.log('Starting SickRage [{branch}] using \'{config}\''.format
(branch=sickbeard.BRANCH, config=sickbeard.CONFIG_FILE))
self.clear_cache()
if self.forced_port:
logger.log('Forcing web server to port {port}'.format(port=self.forced_port))
self.start_port = self.forced_port
else:
self.start_port = sickbeard.WEB_PORT
if sickbeard.WEB_LOG:
self.log_dir = sickbeard.LOG_DIR
else:
self.log_dir = None
# sickbeard.WEB_HOST is available as a configuration value in various
# places but is not configurable. It is supported here for historic reasons.
if sickbeard.WEB_HOST and sickbeard.WEB_HOST != '0.0.0.0':
self.web_host = sickbeard.WEB_HOST
else:
self.web_host = '' if sickbeard.WEB_IPV6 else '0.0.0.0'
# web server options
self.web_options = {
'port': int(self.start_port),
'host': self.web_host,
'data_root': ek(os.path.join, sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME),
'web_root': sickbeard.WEB_ROOT,
'log_dir': self.log_dir,
'username': sickbeard.WEB_USERNAME,
'password': sickbeard.WEB_PASSWORD,
'enable_https': sickbeard.ENABLE_HTTPS,
'handle_reverse_proxy': sickbeard.HANDLE_REVERSE_PROXY,
'https_cert': ek(os.path.join, sickbeard.PROG_DIR, sickbeard.HTTPS_CERT),
'https_key': ek(os.path.join, sickbeard.PROG_DIR, sickbeard.HTTPS_KEY),
}
# start web server
self.web_server = SRWebServer(self.web_options)
self.web_server.start()
# Fire up all our threads
sickbeard.start()
# Build internal name cache
name_cache.buildNameCache()
# Pre-populate network timezones, it isn't thread safe
network_timezones.update_network_dict()
# sure, why not?
if sickbeard.USE_FAILED_DOWNLOADS:
failed_history.trimHistory()
# Check for metadata indexer updates for shows (sets the next aired ep!)
# sickbeard.showUpdateScheduler.forceRun()
# Launch browser
if sickbeard.LAUNCH_BROWSER and not (self.no_launch or self.run_as_daemon):
sickbeard.launchBrowser('https' if sickbeard.ENABLE_HTTPS else 'http', self.start_port, sickbeard.WEB_ROOT)
# main loop
while True:
time.sleep(1)
def daemonize(self):
"""
Fork off as a daemon
"""
# pylint: disable=protected-access
# An object is accessed for a non-existent member.
# Access to a protected member of a client class
# Make a non-session-leader child process
try:
pid = os.fork() # @UndefinedVariable - only available in UNIX
if pid != 0:
os._exit(0)
except OSError as error:
sys.stderr.write('fork #1 failed: {error_num}: {error_message}\n'.format
(error_num=error.errno, error_message=error.strerror))
sys.exit(1)
os.setsid() # @UndefinedVariable - only available in UNIX
# https://github.com/SickRage/SickRage/issues/2969
# http://www.microhowto.info/howto/cause_a_process_to_become_a_daemon_in_c.html#idp23920
# https://www.safaribooksonline.com/library/view/python-cookbook/0596001673/ch06s08.html
# Previous code simply set the umask to whatever it was because it was ANDing instead of OR-ing
# Daemons traditionally run with umask 0 anyways and this should not have repercussions
os.umask(0)
# Make the child a session-leader by detaching from the terminal
try:
pid = os.fork() # @UndefinedVariable - only available in UNIX
if pid != 0:
os._exit(0)
except OSError as error:
sys.stderr.write('fork #2 failed: Error {error_num}: {error_message}\n'.format
(error_num=error.errno, error_message=error.strerror))
sys.exit(1)
# Write pid
if self.create_pid:
pid = os.getpid()
logger.log('Writing PID: {pid} to {filename}'.format(pid=pid, filename=self.pid_file))
try:
with io.open(self.pid_file, 'w') as f_pid:
f_pid.write('{0}\n'.format(pid))
except EnvironmentError as error:
logger.log_error_and_exit('Unable to write PID file: {filename} Error {error_num}: {error_message}'.format
(filename=self.pid_file, error_num=error.errno, error_message=error.strerror))
# Redirect all output
sys.stdout.flush()
sys.stderr.flush()
devnull = getattr(os, 'devnull', '/dev/null')
stdin = file(devnull)
stdout = file(devnull, 'a+')
stderr = file(devnull, 'a+')
os.dup2(stdin.fileno(), getattr(sys.stdin, 'device', sys.stdin).fileno())
os.dup2(stdout.fileno(), getattr(sys.stdout, 'device', sys.stdout).fileno())
os.dup2(stderr.fileno(), getattr(sys.stderr, 'device', sys.stderr).fileno())
@staticmethod
def remove_pid_file(pid_file):
"""
Remove pid file
:param pid_file: to remove
:return:
"""
try:
if ek(os.path.exists, pid_file):
ek(os.remove, pid_file)
except EnvironmentError:
return False
return True
@staticmethod
def load_shows_from_db():
"""
Populates the showList with shows from the database
"""
logger.log('Loading initial show list', logger.DEBUG)
main_db_con = db.DBConnection()
sql_results = main_db_con.select('SELECT indexer, indexer_id, location FROM tv_shows;')
sickbeard.showList = []
for sql_show in sql_results:
try:
cur_show = TVShow(sql_show[b'indexer'], sql_show[b'indexer_id'])
cur_show.nextEpisode()
sickbeard.showList.append(cur_show)
except Exception as error: # pylint: disable=broad-except
logger.log('There was an error creating the show in {0}: Error {1}'.format
(sql_show[b'location'], error), logger.ERROR)
logger.log(traceback.format_exc(), logger.DEBUG)
@staticmethod
def restore_db(src_dir, dst_dir):
"""
Restore the Database from a backup
:param src_dir: Directory containing backup
:param dst_dir: Directory to restore to
:return:
"""
try:
files_list = ['sickbeard.db', 'config.ini', 'failed.db', 'cache.db']
for filename in files_list:
src_file = ek(os.path.join, src_dir, filename)
dst_file = ek(os.path.join, dst_dir, filename)
bak_file = ek(os.path.join, dst_dir, '{0}.bak-{1}'.format(filename, datetime.datetime.now().strftime('%Y%m%d_%H%M%S')))
if ek(os.path.isfile, dst_file):
shutil.move(dst_file, bak_file)
shutil.move(src_file, dst_file)
return True
except Exception: # pylint: disable=broad-except
return False
def shutdown(self, event):
"""
Shut down SickRage
:param event: Type of shutdown event, used to see if restart required
"""
if sickbeard.started:
sickbeard.halt() # stop all tasks
sickbeard.saveAll() # save all shows to DB
# shutdown web server
if self.web_server:
logger.log('Shutting down Tornado')
self.web_server.shutDown()
try:
self.web_server.join(10)
except Exception: # pylint: disable=broad-except
pass
self.clear_cache() # Clean cache
# if run as daemon delete the pid file
if self.run_as_daemon and self.create_pid:
self.remove_pid_file(self.pid_file)
if event == sickbeard.event_queue.Events.SystemEvent.RESTART:
install_type = sickbeard.versionCheckScheduler.action.install_type
popen_list = []
if install_type in ('git', 'source'):
popen_list = [sys.executable, sickbeard.MY_FULLNAME]
elif install_type == 'win':
logger.log('You are using a binary Windows build of SickRage. '
'Please switch to using git.', logger.ERROR)
if popen_list and not sickbeard.NO_RESTART:
popen_list += sickbeard.MY_ARGS
if '--nolaunch' not in popen_list:
popen_list += ['--nolaunch']
logger.log('Restarting SickRage with {options}'.format(options=popen_list))
# shutdown the logger to make sure it's released the logfile BEFORE it restarts SR.
logger.shutdown()
subprocess.Popen(popen_list, cwd=os.getcwd())
# Make sure the logger has stopped, just in case
logger.shutdown()
os._exit(0) # pylint: disable=protected-access
if __name__ == '__main__':
# start SickRage
SickRage().start()