diff --git a/data/interfaces/default/config_postProcessing.tmpl b/data/interfaces/default/config_postProcessing.tmpl index ae3ac4516f1ac3eea0d0f5047f17a160e4927d9f..5f4df17234ac204af0877c4d785f6f63f7b81736 100644 --- a/data/interfaces/default/config_postProcessing.tmpl +++ b/data/interfaces/default/config_postProcessing.tmpl @@ -135,27 +135,27 @@ </label> </div> - <label class="nocheck clearfix" for="process_method"> - <span class="component-title">Process Episode Method:</span> - <span class="component-desc"> - <select name="process_method" id="process_method" class="input-medium" > - #set $process_method_text = {'copy': "Copy", 'move': "Move"} - #for $curAction in ('copy', 'move'): - #if $sickbeard.PROCESS_METHOD == $curAction: - #set $process_method = "selected=\"selected\"" - #else - #set $process_method = "" - #end if - <option value="$curAction" $process_method>$process_method_text[$curAction]</option> - #end for - </select> - </span> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">What method should be used to put the renamed file in the TV directory?</span> - </label> - + <label class="nocheck clearfix" for="process_method"> + <span class="component-title">Process Episode Method:</span> + <span class="component-desc"> + <select name="process_method" id="process_method" class="input-medium" > + #set $process_method_text = {'copy': "Copy", 'move': "Move", 'hardlink': "Physical Link", 'symlink' : "Symbolic Link"} ++ #for $curAction in ('copy', 'move', 'hardlink', 'symlink'): + #if $sickbeard.PROCESS_METHOD == $curAction: + #set $process_method = "selected=\"selected\"" + #else + #set $process_method = "" + #end if + <option value="$curAction" $process_method>$process_method_text[$curAction]</option> + #end for + </select> + </span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">What method should be used to put the renamed file in the TV directory?</span> + </label> + <div class="clearfix"></div> <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> diff --git a/data/interfaces/default/home.tmpl b/data/interfaces/default/home.tmpl index c729955fead0fb0bd83e1e8fc3e759e3036f5cc1..902891dcce4136e01c3f637ea1c642020a2f1c38 100644 --- a/data/interfaces/default/home.tmpl +++ b/data/interfaces/default/home.tmpl @@ -109,9 +109,9 @@ \$("#showListTable:has(tbody tr)").tablesorter({ #if ($layout == 'poster'): - sortList: [[7,0],[2,0]], + sortList: [[7,1],[2,0]], #else: - sortList: [[6,0],[1,0]], + sortList: [[6,1],[1,0]], #end if textExtraction: { #if ( $layout == 'poster'): diff --git a/data/interfaces/default/inc_top.tmpl b/data/interfaces/default/inc_top.tmpl index 9fc81fc1c8b21f531ff3c216f808b3f3e6b9bcc4..974fa672c8448995eff49a9171e14eef4fb7df5c 100644 --- a/data/interfaces/default/inc_top.tmpl +++ b/data/interfaces/default/inc_top.tmpl @@ -80,7 +80,7 @@ \$("#SubMenu a:contains('Clear History')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear History </a>'); \$("#SubMenu a:contains('Trim History')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trim History </a>'); \$("#SubMenu a:contains('Trunc Episode Links')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trunc Episode Links </a>'); - \$("#SubMenu a:contains('Trunc Episode List Processed')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trunc Episode List Processed </a>'); + \$("#SubMenu a:contains('Trunc Episode List Processed')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trunc Episode List Processed </a>'); \$("#SubMenu a[href='/errorlogs/clearerrors']").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear Errors </a>'); \$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Re-scan </a>'); \$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Backlog Overview </a>'); diff --git a/lib/linktastic/README.txt b/lib/linktastic/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..7fad24dbd00df0e67c167c9d40755c27e65cb98d --- /dev/null +++ b/lib/linktastic/README.txt @@ -0,0 +1,19 @@ +Linktastic + +Linktastic is an extension of the os.link and os.symlink functionality provided +by the python language since version 2. Python only supports file linking on +*NIX-based systems, even though it is relatively simple to engineer a solution +to utilize NTFS's built-in linking functionality. Linktastic attempts to unify +linking on the windows platform with linking on *NIX-based systems. + +Usage + +Linktastic is a single python module and can be imported as such. Examples: + +# Hard linking src to dest +import linktastic +linktastic.link(src, dest) + +# Symlinking src to dest +import linktastic +linktastic.symlink(src, dest) diff --git a/lib/linktastic/__init__.py b/lib/linktastic/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/linktastic/linktastic.py b/lib/linktastic/linktastic.py new file mode 100644 index 0000000000000000000000000000000000000000..76687666e61b390aaff84dc3b954fba9b444dbfa --- /dev/null +++ b/lib/linktastic/linktastic.py @@ -0,0 +1,76 @@ +# Linktastic Module +# - A python2/3 compatible module that can create hardlinks/symlinks on windows-based systems +# +# Linktastic is distributed under the MIT License. The follow are the terms and conditions of using Linktastic. +# +# The MIT License (MIT) +# Copyright (c) 2012 Solipsis Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +# associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial +# portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import subprocess +from subprocess import CalledProcessError +import os + + +# Prevent spaces from messing with us! +def _escape_param(param): + return '"%s"' % param + + +# Private function to create link on nt-based systems +def _link_windows(src, dest): + try: + subprocess.check_output( + 'cmd /C mklink /H %s %s' % (_escape_param(dest), _escape_param(src)), + stderr=subprocess.STDOUT) + except CalledProcessError as err: + + raise IOError(err.output.decode('utf-8')) + + # TODO, find out what kind of messages Windows sends us from mklink + # print(stdout) + # assume if they ret-coded 0 we're good + + +def _symlink_windows(src, dest): + try: + subprocess.check_output( + 'cmd /C mklink %s %s' % (_escape_param(dest), _escape_param(src)), + stderr=subprocess.STDOUT) + except CalledProcessError as err: + raise IOError(err.output.decode('utf-8')) + + # TODO, find out what kind of messages Windows sends us from mklink + # print(stdout) + # assume if they ret-coded 0 we're good + + +# Create a hard link to src named as dest +# This version of link, unlike os.link, supports nt systems as well +def link(src, dest): + if os.name == 'nt': + _link_windows(src, dest) + else: + os.link(src, dest) + + +# Create a symlink to src named as dest, but don't fail if you're on nt +def symlink(src, dest): + if os.name == 'nt': + _symlink_windows(src, dest) + else: + os.symlink(src, dest) diff --git a/lib/linktastic/setup.py b/lib/linktastic/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..e15cc2b7f3d4cd86b9750d1e25758a75e90aeb71 --- /dev/null +++ b/lib/linktastic/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup + +setup( + name='Linktastic', + version='0.1.0', + author='Jon "Berkona" Monroe', + author_email='solipsis.dev@gmail.com', + py_modules=['linktastic'], + url="http://github.com/berkona/linktastic", + license='MIT License - See http://opensource.org/licenses/MIT for details', + description='Truly platform-independent file linking', + long_description=open('README.txt').read(), +) diff --git a/lib/subliminal/api.py b/lib/subliminal/api.py index f95fda3b482aa4b7f6b9fbe7e5d0eb6e54d4c290..3b6f9139d25a8afd336b25a0c944e1442102b38e 100644 --- a/lib/subliminal/api.py +++ b/lib/subliminal/api.py @@ -94,10 +94,7 @@ def download_subtitles(paths, languages=None, services=None, force=True, multi=F order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE] subtitles_by_video = list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter) for video, subtitles in subtitles_by_video.iteritems(): - try: - subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True) - except StopIteration: - break + subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True) results = [] service_instances = {} tasks = create_download_tasks(subtitles_by_video, languages, multi) diff --git a/lib/subliminal/core.py b/lib/subliminal/core.py index 80c7f024f8b257f6f4756a113c9e8dcbde489d2c..1b8c840d12c761a9e510758c30cd422f278e8c54 100644 --- a/lib/subliminal/core.py +++ b/lib/subliminal/core.py @@ -32,7 +32,7 @@ __all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE', 'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence', 'key_subtitles', 'group_by_video'] logger = logging.getLogger("subliminal") -SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles', 'itasa'] +SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles'] LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4) diff --git a/lib/subliminal/language.py b/lib/subliminal/language.py index 6403bcc0a562e8504348377572be37debe2f6f7f..c89e7abc0b00eddaf0211454a1cd62789ed6e56d 100644 --- a/lib/subliminal/language.py +++ b/lib/subliminal/language.py @@ -619,7 +619,6 @@ LANGUAGES = [('aar', '', 'aa', u'Afar', u'afar'), ('pli', '', 'pi', u'Pali', u'pali'), ('pol', '', 'pl', u'Polish', u'polonais'), ('pon', '', '', u'Pohnpeian', u'pohnpei'), - ('pob', '', 'pb', u'Brazilian Portuguese', u'brazilian portuguese'), ('por', '', 'pt', u'Portuguese', u'portugais'), ('pra', '', '', u'Prakrit languages', u'prâkrit, langues'), ('pro', '', '', u'Provençal, Old (to 1500)', u'provençal ancien (jusqu\'à 1500)'), diff --git a/lib/subliminal/services/__init__.py b/lib/subliminal/services/__init__.py index 9a21666c00716b849886b295957746aaf7236c5e..7cad1cd6a11656bbff8b7835e5bae4997fdd9ee4 100644 --- a/lib/subliminal/services/__init__.py +++ b/lib/subliminal/services/__init__.py @@ -219,10 +219,18 @@ class ServiceBase(object): # TODO: could check if maybe we already have a text file and # download it directly raise DownloadFailedError('Downloaded file is not a zip file') +# with zipfile.ZipFile(zippath) as zipsub: +# for subfile in zipsub.namelist(): +# if os.path.splitext(subfile)[1] in EXTENSIONS: +# with open(filepath, 'w') as f: +# f.write(zipsub.open(subfile).read()) +# break +# else: +# raise DownloadFailedError('No subtitles found in zip file') zipsub = zipfile.ZipFile(zippath) for subfile in zipsub.namelist(): if os.path.splitext(subfile)[1] in EXTENSIONS: - with open(filepath, 'wb') as f: + with open(filepath, 'w') as f: f.write(zipsub.open(subfile).read()) break else: diff --git a/lib/subliminal/services/addic7ed.py b/lib/subliminal/services/addic7ed.py index 6f7f0f879033eadf4be5344aff06646536ff6931..1080cb479830a298b0328ca696dcfde1ea5a6894 100644 --- a/lib/subliminal/services/addic7ed.py +++ b/lib/subliminal/services/addic7ed.py @@ -38,12 +38,13 @@ class Addic7ed(ServiceBase): api_based = False #TODO: Complete this languages = language_set(['ar', 'ca', 'de', 'el', 'en', 'es', 'eu', 'fr', 'ga', 'gl', 'he', 'hr', 'hu', - 'it', 'pl', 'pt', 'ro', 'ru', 'se', 'pb']) - language_map = {'Portuguese (Brazilian)': Language('pob'), 'Greek': Language('gre'), + 'it', 'pl', 'pt', 'ro', 'ru', 'se', 'pt-br']) + language_map = {'Portuguese (Brazilian)': Language('por-BR'), 'Greek': Language('gre'), 'Spanish (Latin America)': Language('spa'), 'Galego': Language('glg'), u'Català': Language('cat')} videos = [Episode] require_video = False + required_features = ['permissive'] @cachedmethod def get_series_id(self, name): @@ -63,7 +64,6 @@ class Addic7ed(ServiceBase): return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode) def query(self, filepath, languages, keywords, series, season, episode): - logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages)) self.init_cache() try: diff --git a/lib/subliminal/services/opensubtitles.py b/lib/subliminal/services/opensubtitles.py index 65599d24502bde869d8c8782cc81d90e79487c02..fba8e4091d59a898d9d0984385ee82c48953d089 100644 --- a/lib/subliminal/services/opensubtitles.py +++ b/lib/subliminal/services/opensubtitles.py @@ -74,9 +74,9 @@ class OpenSubtitles(ServiceBase): 'twi', 'tyv', 'udm', 'uga', 'uig', 'ukr', 'umb', 'urd', 'uzb', 'vai', 'ven', 'vie', 'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel', 'wen', 'wln', 'wol', 'xal', 'xho', 'yao', 'yap', 'yid', 'yor', 'ypk', 'zap', 'zen', 'zha', 'znd', 'zul', 'zun', - 'pob', 'rum-MD']) - language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), - Language('rum-MD'): 'mol', Language('srp'): 'scc'} + 'por-BR', 'rum-MD']) + language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), 'pob': Language('por-BR'), + Language('rum-MD'): 'mol', Language('srp'): 'scc', Language('por-BR'): 'pob'} language_code = 'alpha3' videos = [Episode, Movie] require_video = False diff --git a/lib/subliminal/services/podnapisi.py b/lib/subliminal/services/podnapisi.py index be02dd51d5b13dd9dd1b3e67718cff2026da8d61..108de211bae5aef010fdfd19cae27960b65b8d3e 100644 --- a/lib/subliminal/services/podnapisi.py +++ b/lib/subliminal/services/podnapisi.py @@ -37,10 +37,10 @@ class Podnapisi(ServiceBase): 'es', 'et', 'fa', 'fi', 'fr', 'ga', 'he', 'hi', 'hr', 'hu', 'id', 'is', 'it', 'ja', 'ko', 'lt', 'lv', 'mk', 'ms', 'nl', 'nn', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sq', 'sr', 'sv', 'th', 'tr', 'uk', - 'vi', 'zh', 'es-ar', 'pb']) + 'vi', 'zh', 'es-ar', 'pt-br']) language_map = {'jp': Language('jpn'), Language('jpn'): 'jp', 'gr': Language('gre'), Language('gre'): 'gr', -# 'pb': Language('por-BR'), Language('por-BR'): 'pb', + 'pb': Language('por-BR'), Language('por-BR'): 'pb', 'ag': Language('spa-AR'), Language('spa-AR'): 'ag', 'cyr': Language('srp')} videos = [Episode, Movie] diff --git a/lib/subliminal/services/subswiki.py b/lib/subliminal/services/subswiki.py index 9f9a341413b91bdc24f2803743f6b94f095f1662..2a3d57f8a55c0fd24a46fcab69605107f16b3e32 100644 --- a/lib/subliminal/services/subswiki.py +++ b/lib/subliminal/services/subswiki.py @@ -33,9 +33,9 @@ class SubsWiki(ServiceBase): server_url = 'http://www.subswiki.com' site_url = 'http://www.subswiki.com' api_based = False - languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'pob', 'por', 'spa-ES', u'spa', u'ita', u'cat']) + languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat']) language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'), - u'Català': Language('cat'), u'Brazilian': Language('pob'), u'English (US)': Language('eng-US'), + u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'), u'English (UK)': Language('eng-GB')} language_code = 'name' videos = [Episode, Movie] diff --git a/lib/subliminal/services/subtitulos.py b/lib/subliminal/services/subtitulos.py index 6dd085a3b4eb41ad1056f8f8182b21806ca47e54..103b241c9797eb6241c1be0e3efd54d73adbf159 100644 --- a/lib/subliminal/services/subtitulos.py +++ b/lib/subliminal/services/subtitulos.py @@ -34,9 +34,9 @@ class Subtitulos(ServiceBase): server_url = 'http://www.subtitulos.es' site_url = 'http://www.subtitulos.es' api_based = False - languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'pob', 'por', 'spa-ES', u'spa', u'ita', u'cat']) + languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat']) language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), #u'Español (Latinoamérica)': Language('spa'), - u'Català': Language('cat'), u'Brazilian': Language('pob'), u'English (US)': Language('eng-US'), + u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'), u'English (UK)': Language('eng-GB'), 'Galego': Language('glg')} language_code = 'name' videos = [Episode] @@ -52,7 +52,7 @@ class Subtitulos(ServiceBase): return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode) def query(self, filepath, languages, keywords, series, season, episode): - request_series = series.lower().replace(' ', '-').replace('&', '@').replace('(','').replace(')','') + request_series = series.lower().replace(' ', '_').replace('&', '@').replace('(','').replace(')','') if isinstance(request_series, unicode): request_series = unicodedata.normalize('NFKD', request_series).encode('ascii', 'ignore') logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages)) diff --git a/lib/subliminal/services/thesubdb.py b/lib/subliminal/services/thesubdb.py index 93787ad62e203ed3aa0c5c9e7dc0bfe89fb1d880..9d2ced82bfcc5904721787d2dfbc738e2f96fc68 100644 --- a/lib/subliminal/services/thesubdb.py +++ b/lib/subliminal/services/thesubdb.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with subliminal. If not, see <http://www.gnu.org/licenses/>. from . import ServiceBase -from ..language import language_set, Language +from ..language import language_set from ..subtitles import get_subtitle_path, ResultSubtitle from ..videos import Episode, Movie, UnknownVideo import logging @@ -32,7 +32,7 @@ class TheSubDB(ServiceBase): api_based = True # Source: http://api.thesubdb.com/?action=languages languages = language_set(['af', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'id', 'it', - 'la', 'nl', 'no', 'oc', 'pl', 'pb', 'ro', 'ru', 'sl', 'sr', 'sv', + 'la', 'nl', 'no', 'oc', 'pl', 'pt', 'ro', 'ru', 'sl', 'sr', 'sv', 'tr']) videos = [Movie, Episode, UnknownVideo] require_video = True @@ -49,10 +49,6 @@ class TheSubDB(ServiceBase): logger.error(u'Request %s returned status code %d' % (r.url, r.status_code)) return [] available_languages = language_set(r.content.split(',')) - #this is needed becase for theSubDB pt languages is Portoguese Brazil and not Portoguese# - #So we are deleting pt language and adding pb language - if Language('pt') in available_languages: - available_languages = available_languages - language_set(['pt']) | language_set(['pb']) languages &= available_languages if not languages: logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages)) diff --git a/lib/subliminal/services/tvsubtitles.py b/lib/subliminal/services/tvsubtitles.py index f6b2fd52b6621fff7121c38986f95fb7c09ae8cd..27992226d2d0b82d09bf4c4f2afa007a61624761 100644 --- a/lib/subliminal/services/tvsubtitles.py +++ b/lib/subliminal/services/tvsubtitles.py @@ -43,10 +43,10 @@ class TvSubtitles(ServiceBase): api_based = False languages = language_set(['ar', 'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk', - 'zh', 'pb']) + 'zh', 'pt-br']) #TODO: Find more exceptions language_map = {'gr': Language('gre'), 'cz': Language('cze'), 'ua': Language('ukr'), - 'cn': Language('chi'), 'br': Language('pob')} + 'cn': Language('chi')} videos = [Episode] require_video = False required_features = ['permissive'] diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 3b42989bf90c8389ce1e26075c4e59e60709475f..bb9db86be7d329d4afc7ae8f906a9dc3a30f36cb 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -182,7 +182,7 @@ RENAME_EPISODES = False PROCESS_AUTOMATICALLY = False PROCESS_AUTOMATICALLY_TORRENT = False KEEP_PROCESSED_DIR = False -PROCESS_METHOD = None +PROCESS_METHOD = None MOVE_ASSOCIATED_FILES = False TV_DOWNLOAD_DIR = None TORRENT_DOWNLOAD_DIR = None @@ -438,7 +438,7 @@ def initialize(consoleLogging=True): USE_SYNOLOGYNOTIFIER, SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH, SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD, SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD, \ USE_MAIL, MAIL_USERNAME, MAIL_PASSWORD, MAIL_SERVER, MAIL_SSL, MAIL_FROM, MAIL_TO, MAIL_NOTIFY_ONSNATCH, \ NZBMATRIX_APIKEY, versionCheckScheduler, VERSION_NOTIFY, PROCESS_AUTOMATICALLY, PROCESS_AUTOMATICALLY_TORRENT, \ - KEEP_PROCESSED_DIR, PROCESS_METHOD, TV_DOWNLOAD_DIR, TORRENT_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ + KEEP_PROCESSED_DIR, PROCESS_METHOD, TV_DOWNLOAD_DIR, TORRENT_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TVDB_API_PARMS, \ NAMING_PATTERN, NAMING_MULTI_EP, NAMING_FORCE_FOLDERS, NAMING_ABD_PATTERN, NAMING_CUSTOM_ABD, \ RENAME_EPISODES, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, \ @@ -527,7 +527,7 @@ def initialize(consoleLogging=True): TVDB_BASE_URL = 'http://thetvdb.com/api/' + TVDB_API_KEY - TOGGLE_SEARCH = check_setting_int(CFG, 'General', 'toggle_search', '') + TOGGLE_SEARCH = check_setting_int(CFG, 'General', 'toggle_search', '') QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) AUDIO_SHOW_DEFAULT = check_setting_str(CFG, 'General', 'audio_show_default', 'fr' ) @@ -571,7 +571,7 @@ def initialize(consoleLogging=True): PROCESS_AUTOMATICALLY_TORRENT = check_setting_int(CFG, 'General', 'process_automatically_torrent', 0) RENAME_EPISODES = check_setting_int(CFG, 'General', 'rename_episodes', 1) KEEP_PROCESSED_DIR = check_setting_int(CFG, 'General', 'keep_processed_dir', 1) - PROCESS_METHOD = check_setting_str(CFG, 'General', 'process_method', 'copy' if KEEP_PROCESSED_DIR else 'move') + PROCESS_METHOD = check_setting_str(CFG, 'General', 'process_method', 'copy' if KEEP_PROCESSED_DIR else 'move') MOVE_ASSOCIATED_FILES = check_setting_int(CFG, 'General', 'move_associated_files', 0) CREATE_MISSING_SHOW_DIRS = check_setting_int(CFG, 'General', 'create_missing_show_dirs', 0) ADD_SHOWS_WO_DIR = check_setting_int(CFG, 'General', 'add_shows_wo_dir', 0) @@ -1300,7 +1300,7 @@ def save_config(): new_config['General']['tv_download_dir'] = TV_DOWNLOAD_DIR new_config['General']['torrent_download_dir'] = TORRENT_DOWNLOAD_DIR new_config['General']['keep_processed_dir'] = int(KEEP_PROCESSED_DIR) - new_config['General']['process_method'] = PROCESS_METHOD + new_config['General']['process_method'] = PROCESS_METHOD new_config['General']['move_associated_files'] = int(MOVE_ASSOCIATED_FILES) new_config['General']['process_automatically'] = int(PROCESS_AUTOMATICALLY) new_config['General']['process_automatically_torrent'] = int(PROCESS_AUTOMATICALLY_TORRENT) diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index ef67fcafb8fd5d811a51263b3e75808e672bbc6d..c45cf0fa9cb64cd14c6871ea017774ad071e8625 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -25,7 +25,7 @@ from sickbeard.providers.generic import GenericProvider from sickbeard import encodingKludge as ek from sickbeard.name_parser.parser import NameParser, InvalidNameException -MAX_DB_VERSION = 16 +MAX_DB_VERSION = 16 class MainSanityCheck(db.DBSanityCheck): @@ -103,7 +103,7 @@ class InitialSchema (db.SchemaUpgrade): "CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC, quality NUMERIC, resource TEXT, provider NUMERIC);", "CREATE TABLE episode_links (episode_id INTEGER, link TEXT);", "CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC);" - "CREATE TABLE processed_files (episode_id INTEGER, filename TEXT, md5 TEXT)" + "CREATE TABLE processed_files (episode_id INTEGER, filename TEXT, md5 TEXT)" ] for query in queries: self.connection.action(query) @@ -717,7 +717,7 @@ class AddEpisodeLinkTable(AddSubtitlesSupport): if self.hasTable("episode_links") != True: self.connection.action("CREATE TABLE episode_links (episode_id INTEGER, link TEXT)") self.incDBVersion() - + class AddIMDbInfo(AddEpisodeLinkTable): def test(self): return self.checkDBVersion() >= 15 @@ -725,13 +725,13 @@ class AddIMDbInfo(AddEpisodeLinkTable): def execute(self): if self.hasTable("imdb_info") != True: self.connection.action("CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)") - self.incDBVersion() - -class AddProcessedFilesTable(AddIMDbInfo): - def test(self): - return self.checkDBVersion() >= 16 - - def execute(self): - if self.hasTable("processed_files") != True: - self.connection.action("CREATE TABLE processed_files (episode_id INTEGER, filename TEXT, md5 TEXT)") + self.incDBVersion() + +class AddProcessedFilesTable(AddIMDbInfo): + def test(self): + return self.checkDBVersion() >= 16 + + def execute(self): + if self.hasTable("processed_files") != True: + self.connection.action("CREATE TABLE processed_files (episode_id INTEGER, filename TEXT, md5 TEXT)") self.incDBVersion() \ No newline at end of file diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 0114f62918bd373be0524a5b4c28b20ab92ef84f..a19411d5addbfe8f2318e8f596cce173e35098ee 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -24,6 +24,7 @@ import re, socket import shutil import traceback import time, sys +from lib.linktastic import linktastic import hashlib @@ -41,7 +42,7 @@ from sickbeard import db from sickbeard import encodingKludge as ek from sickbeard import notifiers -#from lib.linktastic import linktastic +#from lib.linktastic import linktastic from lib.tvdb_api import tvdb_api, tvdb_exceptions import xml.etree.cElementTree as etree @@ -481,25 +482,25 @@ def moveFile(srcFile, destFile): copyFile(srcFile, destFile) ek.ek(os.unlink, srcFile) -# def hardlinkFile(srcFile, destFile): -# try: -# ek.ek(linktastic.link, srcFile, destFile) -# fixSetGroupID(destFile) -# except OSError: -# logger.log(u"Failed to create hardlink of " + srcFile + " at " + destFile + ". Copying instead", logger.ERROR) -# copyFile(srcFile, destFile) -# ek.ek(os.unlink, srcFile) -# -# def moveAndSymlinkFile(srcFile, destFile): -# try: -# ek.ek(os.rename, srcFile, destFile) -# fixSetGroupID(destFile) -# ek.ek(linktastic.symlink, destFile, srcFile) -# except OSError: -# logger.log(u"Failed to create symlink of " + srcFile + " at " + destFile + ". Copying instead", logger.ERROR) -# copyFile(srcFile, destFile) -# ek.ek(os.unlink, srcFile) - +def hardlinkFile(srcFile, destFile): + try: + ek.ek(linktastic.link, srcFile, destFile) + fixSetGroupID(destFile) + except OSError: + logger.log(u"Failed to create hardlink of " + srcFile + " at " + destFile + ". Copying instead", logger.ERROR) + copyFile(srcFile, destFile) + ek.ek(os.unlink, srcFile) + +def moveAndSymlinkFile(srcFile, destFile): + try: + ek.ek(os.rename, srcFile, destFile) + fixSetGroupID(destFile) + ek.ek(linktastic.symlink, destFile, srcFile) + except OSError: + logger.log(u"Failed to create symlink of " + srcFile + " at " + destFile + ". Copying instead", logger.ERROR) + copyFile(srcFile, destFile) + ek.ek(os.unlink, srcFile) + def del_empty_dirs(s_dir): b_empty = True @@ -512,8 +513,9 @@ def del_empty_dirs(s_dir): b_empty = False if b_empty: - logger.log(u"Deleting " + s_dir.decode('utf-8')+ ' because it is empty') - os.rmdir(s_dir) + if s_dir!=sickbeard.TORRENT_DOWNLOAD_DIR and s_dir!=sickbeard.TV_DOWNLOAD_DIR: + logger.log(u"Deleting " + s_dir.decode('utf-8')+ ' because it is empty') + os.rmdir(s_dir) def make_dirs(path): """ diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index bb5155a6e96516a9f2f3acc7254d84e530ba0519..e8d8dcb0956b152413cd16e9961c9ddfa9f7126b 100755 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -1,1025 +1,1031 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import glob -import os -import re -import shlex -import subprocess - -import sickbeard -import hashlib - -from sickbeard import db -from sickbeard import classes -from sickbeard import common -from sickbeard import exceptions -from sickbeard import helpers -from sickbeard import history -from sickbeard import logger -from sickbeard import notifiers -from sickbeard import show_name_helpers -from sickbeard import scene_exceptions - -from sickbeard import encodingKludge as ek -from sickbeard.exceptions import ex - -from sickbeard.name_parser.parser import NameParser, InvalidNameException - -from lib.tvdb_api import tvdb_api, tvdb_exceptions - -class PostProcessor(object): - """ - A class which will process a media file according to the post processing settings in the config. - """ - - EXISTS_LARGER = 1 - EXISTS_SAME = 2 - EXISTS_SMALLER = 3 - DOESNT_EXIST = 4 - - IGNORED_FILESTRINGS = [ "/.AppleDouble/", ".DS_Store" ] - - NZB_NAME = 1 - FOLDER_NAME = 2 - FILE_NAME = 3 - - def __init__(self, file_path, nzb_name = None): - """ - Creates a new post processor with the given file path and optionally an NZB name. - - file_path: The path to the file to be processed - nzb_name: The name of the NZB which resulted in this file being downloaded (optional) - """ - # absolute path to the folder that is being processed - self.folder_path = ek.ek(os.path.dirname, ek.ek(os.path.abspath, file_path)) - - # full path to file - self.file_path = file_path - - # file name only - self.file_name = ek.ek(os.path.basename, file_path) - - # the name of the folder only - self.folder_name = ek.ek(os.path.basename, self.folder_path) - - # name of the NZB that resulted in this folder - self.nzb_name = nzb_name - - self.in_history = False - self.release_group = None - self.is_proper = False - - self.good_results = {self.NZB_NAME: False, - self.FOLDER_NAME: False, - self.FILE_NAME: False} - - self.log = '' - - def _log(self, message, level=logger.MESSAGE): - """ - A wrapper for the internal logger which also keeps track of messages and saves them to a string for later. - - message: The string to log (unicode) - level: The log level to use (optional) - """ - logger.log(message, level) - self.log += message + '\n' - - def _checkForExistingFile(self, existing_file): - """ - Checks if a file exists already and if it does whether it's bigger or smaller than - the file we are post processing - - existing_file: The file to compare to - - Returns: - DOESNT_EXIST if the file doesn't exist - EXISTS_LARGER if the file exists and is larger than the file we are post processing - EXISTS_SMALLER if the file exists and is smaller than the file we are post processing - EXISTS_SAME if the file exists and is the same size as the file we are post processing - """ - - if not existing_file: - self._log(u"There is no existing file so there's no worries about replacing it", logger.DEBUG) - return PostProcessor.DOESNT_EXIST - - # if the new file exists, return the appropriate code depending on the size - if ek.ek(os.path.isfile, existing_file): - - # see if it's bigger than our old file - if ek.ek(os.path.getsize, existing_file) > ek.ek(os.path.getsize, self.file_path): - self._log(u"File "+existing_file+" is larger than "+self.file_path, logger.DEBUG) - return PostProcessor.EXISTS_LARGER - - elif ek.ek(os.path.getsize, existing_file) == ek.ek(os.path.getsize, self.file_path): - self._log(u"File "+existing_file+" is the same size as "+self.file_path, logger.DEBUG) - return PostProcessor.EXISTS_SAME - - else: - self._log(u"File "+existing_file+" is smaller than "+self.file_path, logger.DEBUG) - return PostProcessor.EXISTS_SMALLER - - else: - self._log(u"File "+existing_file+" doesn't exist so there's no worries about replacing it", logger.DEBUG) - return PostProcessor.DOESNT_EXIST - - def _list_associated_files(self, file_path, subtitles_only=False): - """ - For a given file path searches for files with the same name but different extension and returns their absolute paths - - file_path: The file to check for associated files - - Returns: A list containing all files which are associated to the given file - """ - - if not file_path: - return [] - - file_path_list = [] - dumb_files_list =[] - - base_name = file_path.rpartition('.')[0]+'.' - - # don't strip it all and use cwd by accident - if not base_name: - return [] - - # don't confuse glob with chars we didn't mean to use - base_name = re.sub(r'[\[\]\*\?]', r'[\g<0>]', base_name) - - for associated_file_path in ek.ek(glob.glob, base_name+'*'): - # only add associated to list - if associated_file_path == file_path: - continue - # only list it if the only non-shared part is the extension or if it is a subtitle - - if '.' in associated_file_path[len(base_name):]: - continue - if subtitles_only and not associated_file_path[len(associated_file_path)-3:] in common.subtitleExtensions: - continue - - file_path_list.append(associated_file_path) - - return file_path_list - def _list_dummy_files(self, file_path, oribasename=None,directory=None): - """ - For a given file path searches for dummy files - - Returns: deletes all files which are dummy to the given file - """ - - if not file_path: - return [] - dumb_files_list =[] - if oribasename: - base_name=oribasename - else: - base_name = file_path.rpartition('.')[0]+'.' - - # don't strip it all and use cwd by accident - if not base_name: - return [] - - # don't confuse glob with chars we didn't mean to use - base_name = re.sub(r'[\[\]\*\?]', r'[\g<0>]', base_name) - if directory =="d": - cur_dir=file_path - else: - cur_dir=self.folder_path - ass_files=ek.ek(glob.glob, base_name+'*') - dum_files=ek.ek(glob.glob, cur_dir+'\*') - for dummy_file_path in dum_files: - if os.path.isdir(dummy_file_path): - self._list_dummy_files(dummy_file_path, base_name,"d") - elif dummy_file_path==self.file_path or dummy_file_path[len(dummy_file_path)-3:] in common.mediaExtensions or sickbeard.MOVE_ASSOCIATED_FILES: - continue - else: - dumb_files_list.append(dummy_file_path) - for cur_file in dumb_files_list: - self._log(u"Deleting file "+cur_file, logger.DEBUG) - if ek.ek(os.path.isfile, cur_file): - ek.ek(os.remove, cur_file) - - return - def _delete(self, file_path, associated_files=False): - """ - Deletes the file and optionally all associated files. - - file_path: The file to delete - associated_files: True to delete all files which differ only by extension, False to leave them - """ - - if not file_path: - return - - # figure out which files we want to delete - file_list = [file_path] - self._list_dummy_files(file_path) - if associated_files: - file_list = file_list + self._list_associated_files(file_path) - - if not file_list: - self._log(u"There were no files associated with " + file_path + ", not deleting anything", logger.DEBUG) - return - - # delete the file and any other files which we want to delete - for cur_file in file_list: - self._log(u"Deleting file "+cur_file, logger.DEBUG) - if ek.ek(os.path.isfile, cur_file): - ek.ek(os.remove, cur_file) - # do the library update for synoindex - notifiers.synoindex_notifier.deleteFile(cur_file) - - def _combined_file_operation (self, file_path, new_path, new_base_name, associated_files=False, action=None, subtitles=False): - """ - Performs a generic operation (move or copy) on a file. Can rename the file as well as change its location, - and optionally move associated files too. - - file_path: The full path of the media file to act on - new_path: Destination path where we want to move/copy the file to - new_base_name: The base filename (no extension) to use during the copy. Use None to keep the same name. - associated_files: Boolean, whether we should copy similarly-named files too - action: function that takes an old path and new path and does an operation with them (move/copy) - """ - - if not action: - self._log(u"Must provide an action for the combined file operation", logger.ERROR) - return - - file_list = [file_path] - self._list_dummy_files(file_path) - if associated_files: - file_list = file_list + self._list_associated_files(file_path) - elif subtitles: - file_list = file_list + self._list_associated_files(file_path, True) - - if not file_list: - self._log(u"There were no files associated with " + file_path + ", not moving anything", logger.DEBUG) - return - - # deal with all files - for cur_file_path in file_list: - - cur_file_name = ek.ek(os.path.basename, cur_file_path) - - # get the extension - cur_extension = cur_file_path.rpartition('.')[-1] - - # check if file have language of subtitles - if cur_extension in common.subtitleExtensions: - cur_lang = cur_file_path.rpartition('.')[0].rpartition('.')[-1] - if cur_lang in sickbeard.SUBTITLES_LANGUAGES: - cur_extension = cur_lang + '.' + cur_extension - - # replace .nfo with .nfo-orig to avoid conflicts - if cur_extension == 'nfo': - cur_extension = 'nfo-orig' - - # If new base name then convert name - if new_base_name: - new_file_name = new_base_name +'.' + cur_extension - # if we're not renaming we still want to change extensions sometimes - else: - new_file_name = helpers.replaceExtension(cur_file_name, cur_extension) - - if sickbeard.SUBTITLES_DIR and cur_extension in common.subtitleExtensions: - subs_new_path = ek.ek(os.path.join, new_path, sickbeard.SUBTITLES_DIR) - dir_exists = helpers.makeDir(subs_new_path) - if not dir_exists: - logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) - else: - helpers.chmodAsParent(subs_new_path) - new_file_path = ek.ek(os.path.join, subs_new_path, new_file_name) - else: - if sickbeard.SUBTITLES_DIR_SUB and cur_extension in common.subtitleExtensions: - subs_new_path = os.path.join(os.path.dirname(file.path),"Subs") - dir_exists = helpers.makeDir(subs_new_path) - if not dir_exists: - logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) - else: - helpers.chmodAsParent(subs_new_path) - new_file_path = ek.ek(os.path.join, subs_new_path, new_file_name) - else : - new_file_path = ek.ek(os.path.join, new_path, new_file_name) - - action(cur_file_path, new_file_path) - - def _move(self, file_path, new_path, new_base_name, associated_files=False, subtitles=False): - """ - file_path: The full path of the media file to move - new_path: Destination path where we want to move the file to - new_base_name: The base filename (no extension) to use during the move. Use None to keep the same name. - associated_files: Boolean, whether we should move similarly-named files too - """ - - def _int_move(cur_file_path, new_file_path): - - self._log(u"Moving file from "+cur_file_path+" to "+new_file_path, logger.DEBUG) - try: - helpers.moveFile(cur_file_path, new_file_path) - helpers.chmodAsParent(new_file_path) - except (IOError, OSError), e: - self._log("Unable to move file "+cur_file_path+" to "+new_file_path+": "+ex(e), logger.ERROR) - raise e - - self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_move, subtitles=subtitles) - - def _copy(self, file_path, new_path, new_base_name, associated_files=False, subtitles=False): - """ - file_path: The full path of the media file to copy - new_path: Destination path where we want to copy the file to - new_base_name: The base filename (no extension) to use during the copy. Use None to keep the same name. - associated_files: Boolean, whether we should copy similarly-named files too - """ - - def _int_copy (cur_file_path, new_file_path): - - self._log(u"Copying file from "+cur_file_path+" to "+new_file_path, logger.DEBUG) - try: - helpers.copyFile(cur_file_path, new_file_path) - helpers.chmodAsParent(new_file_path) - except (IOError, OSError), e: - logger.log("Unable to copy file "+cur_file_path+" to "+new_file_path+": "+ex(e), logger.ERROR) - raise e - - self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_copy, subtitles=subtitles) - - def _hardlink(self, file_path, new_path, new_base_name, associated_files=False): - """ - file_path: The full path of the media file to move - new_path: Destination path where we want to create a hard linked file - new_base_name: The base filename (no extension) to use during the link. Use None to keep the same name. - associated_files: Boolean, whether we should move similarly-named files too - """ - - def _int_hard_link(cur_file_path, new_file_path): - - self._log(u"Hard linking file from " + cur_file_path + " to " + new_file_path, logger.DEBUG) - try: - helpers.hardlinkFile(cur_file_path, new_file_path) - helpers.chmodAsParent(new_file_path) - except (IOError, OSError), e: - self._log("Unable to link file " + cur_file_path + " to " + new_file_path + ": "+ex(e), logger.ERROR) - raise e - self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_hard_link) - - def _moveAndSymlink(self, file_path, new_path, new_base_name, associated_files=False): - """ - file_path: The full path of the media file to move - new_path: Destination path where we want to move the file to create a symbolic link to - new_base_name: The base filename (no extension) to use during the link. Use None to keep the same name. - associated_files: Boolean, whether we should move similarly-named files too - """ - - def _int_move_and_sym_link(cur_file_path, new_file_path): - - self._log(u"Moving then symbolic linking file from " + cur_file_path + " to " + new_file_path, logger.DEBUG) - try: - helpers.moveAndSymlinkFile(cur_file_path, new_file_path) - helpers.chmodAsParent(new_file_path) - except (IOError, OSError), e: - self._log("Unable to link file " + cur_file_path + " to " + new_file_path + ": " + ex(e), logger.ERROR) - raise e - self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_move_and_sym_link) - - def _history_lookup(self): - """ - Look up the NZB name in the history and see if it contains a record for self.nzb_name - - Returns a (tvdb_id, season, []) tuple. The first two may be None if none were found. - """ - - to_return = (None, None, []) - - # 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: - self.in_history = False - return to_return - - # make a list of possible names to use in the search - names = [] - if self.nzb_name: - names.append(self.nzb_name) - if '.' in self.nzb_name: - names.append(self.nzb_name.rpartition(".")[0]) - if self.folder_name: - names.append(self.folder_name) - - myDB = db.DBConnection() - - # search the database for a possible match and return immediately if we find one - for curName in names: - sql_results = myDB.select("SELECT * FROM history WHERE resource LIKE ?", [re.sub("[\.\-\ ]", "_", curName)]) - - if len(sql_results) == 0: - continue - - tvdb_id = int(sql_results[0]["showid"]) - season = int(sql_results[0]["season"]) - - self.in_history = True - to_return = (tvdb_id, season, []) - self._log("Found result in history: "+str(to_return), logger.DEBUG) - - if curName == self.nzb_name: - self.good_results[self.NZB_NAME] = True - elif curName == self.folder_name: - self.good_results[self.FOLDER_NAME] = True - elif curName == self.file_name: - self.good_results[self.FILE_NAME] = True - - return to_return - - self.in_history = False - return to_return - - def _analyze_name(self, name, file=True): - """ - Takes a name and tries to figure out a show, season, and episode from it. - - name: A string which we want to analyze to determine show info from (unicode) - - Returns a (tvdb_id, season, [episodes]) tuple. The first two may be None and episodes may be [] - if none were found. - """ - - logger.log(u"Analyzing name "+repr(name)) - - to_return = (None, None, []) - - if not name: - return to_return - - # parse the name to break it into show name, season, and episode - np = NameParser(file) - parse_result = np.parse(name) - self._log("Parsed "+name+" into "+str(parse_result).decode('utf-8'), logger.DEBUG) - - if parse_result.air_by_date: - season = -1 - episodes = [parse_result.air_date] - else: - season = parse_result.season_number - episodes = parse_result.episode_numbers - - to_return = (None, season, episodes) - - # do a scene reverse-lookup to get a list of all possible names - name_list = show_name_helpers.sceneToNormalShowNames(parse_result.series_name) - - if not name_list: - return (None, season, episodes) - - def _finalize(parse_result): - self.release_group = parse_result.release_group - - # remember whether it's a proper - if parse_result.extra_info: - self.is_proper = re.search('(^|[\. _-])(proper|repack)([\. _-]|$)', parse_result.extra_info, re.I) != None - - # if the result is complete then remember that for later - if parse_result.series_name and parse_result.season_number != None and parse_result.episode_numbers and parse_result.release_group: - test_name = os.path.basename(name) - if test_name == self.nzb_name: - self.good_results[self.NZB_NAME] = True - elif test_name == self.folder_name: - self.good_results[self.FOLDER_NAME] = True - elif test_name == self.file_name: - self.good_results[self.FILE_NAME] = True - else: - logger.log(u"Nothing was good, found "+repr(test_name)+" and wanted either "+repr(self.nzb_name)+", "+repr(self.folder_name)+", or "+repr(self.file_name)) - else: - logger.log("Parse result not suficent(all folowing have to be set). will not save release name", logger.DEBUG) - logger.log("Parse result(series_name): " + str(parse_result.series_name), logger.DEBUG) - logger.log("Parse result(season_number): " + str(parse_result.season_number), logger.DEBUG) - logger.log("Parse result(episode_numbers): " + str(parse_result.episode_numbers), logger.DEBUG) - logger.log("Parse result(release_group): " + str(parse_result.release_group), logger.DEBUG) - - # for each possible interpretation of that scene name - for cur_name in name_list: - self._log(u"Checking scene exceptions for a match on "+cur_name, logger.DEBUG) - scene_id = scene_exceptions.get_scene_exception_by_name(cur_name) - if scene_id: - self._log(u"Scene exception lookup got tvdb id "+str(scene_id)+u", using that", logger.DEBUG) - _finalize(parse_result) - return (scene_id, season, episodes) - - # see if we can find the name directly in the DB, if so use it - for cur_name in name_list: - self._log(u"Looking up "+cur_name+u" in the DB", logger.DEBUG) - db_result = helpers.searchDBForShow(cur_name) - if db_result: - self._log(u"Lookup successful, using tvdb id "+str(db_result[0]), logger.DEBUG) - _finalize(parse_result) - return (int(db_result[0]), season, episodes) - - # see if we can find the name with a TVDB lookup - for cur_name in name_list: - try: - t = tvdb_api.Tvdb(custom_ui=classes.ShowListUI, **sickbeard.TVDB_API_PARMS) - - self._log(u"Looking up name "+cur_name+u" on TVDB", logger.DEBUG) - showObj = t[cur_name] - except (tvdb_exceptions.tvdb_exception): - # if none found, search on all languages - try: - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - ltvdb_api_parms['search_all_languages'] = True - t = tvdb_api.Tvdb(custom_ui=classes.ShowListUI, **ltvdb_api_parms) - - self._log(u"Looking up name "+cur_name+u" in all languages on TVDB", logger.DEBUG) - showObj = t[cur_name] - except (tvdb_exceptions.tvdb_exception, IOError): - pass - - continue - except (IOError): - continue - - self._log(u"Lookup successful, using tvdb id "+str(showObj["id"]), logger.DEBUG) - _finalize(parse_result) - return (int(showObj["id"]), season, episodes) - - _finalize(parse_result) - return to_return - - - def _find_info(self): - """ - For a given file try to find the showid, season, and episode. - """ - - tvdb_id = season = None - episodes = [] - - # try to look up the nzb in history - attempt_list = [self._history_lookup, - - # try to analyze the nzb name - lambda: self._analyze_name(self.nzb_name), - - # try to analyze the file name - lambda: self._analyze_name(self.file_name), - - # try to analyze the dir name - lambda: self._analyze_name(self.folder_name), - - # try to analyze the file+dir names together - lambda: self._analyze_name(self.file_path), - - # try to analyze the dir + file name together as one name - lambda: self._analyze_name(self.folder_name + u' ' + self.file_name) - - ] - - # attempt every possible method to get our info - for cur_attempt in attempt_list: - - try: - (cur_tvdb_id, cur_season, cur_episodes) = cur_attempt() - except InvalidNameException, e: - logger.log(u"Unable to parse, skipping: "+ex(e), logger.DEBUG) - continue - - # if we already did a successful history lookup then keep that tvdb_id value - if cur_tvdb_id and not (self.in_history and tvdb_id): - tvdb_id = cur_tvdb_id - if cur_season != None: - season = cur_season - if cur_episodes: - episodes = cur_episodes - - # for air-by-date shows we need to look up the season/episode from tvdb - if season == -1 and tvdb_id and episodes: - self._log(u"Looks like this is an air-by-date show, attempting to convert the date to season/episode", logger.DEBUG) - - # try to get language set for this show - tvdb_lang = None - try: - showObj = helpers.findCertainShow(sickbeard.showList, tvdb_id) - if(showObj != None): - tvdb_lang = showObj.lang - except exceptions.MultipleShowObjectsException: - raise #TODO: later I'll just log this, for now I want to know about it ASAP - - try: - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if tvdb_lang and not tvdb_lang == 'en': - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - epObj = t[tvdb_id].airedOn(episodes[0])[0] - season = int(epObj["seasonnumber"]) - episodes = [int(epObj["episodenumber"])] - self._log(u"Got season " + str(season) + " episodes " + str(episodes), logger.DEBUG) - except tvdb_exceptions.tvdb_episodenotfound, e: - self._log(u"Unable to find episode with date " + str(episodes[0]) + u" for show " + str(tvdb_id) + u", skipping", logger.DEBUG) - # we don't want to leave dates in the episode list if we couldn't convert them to real episode numbers - episodes = [] - continue - except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to contact TVDB: " + ex(e), logger.WARNING) - episodes = [] - continue - - # if there's no season then we can hopefully just use 1 automatically - elif season == None and tvdb_id: - myDB = db.DBConnection() - numseasonsSQlResult = myDB.select("SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0", [tvdb_id]) - if int(numseasonsSQlResult[0][0]) == 1 and season == None: - self._log(u"Don't have a season number, but this show appears to only have 1 season, setting seasonnumber to 1...", logger.DEBUG) - season = 1 - - if tvdb_id and season != None and episodes: - return (tvdb_id, season, episodes) - - return (tvdb_id, season, episodes) - - def _get_ep_obj(self, tvdb_id, season, episodes): - """ - Retrieve the TVEpisode object requested. - - tvdb_id: The TVDBID of the show (int) - season: The season of the episode (int) - episodes: A list of episodes to find (list of ints) - - If the episode(s) can be found then a TVEpisode object with the correct related eps will - be instantiated and returned. If the episode can't be found then None will be returned. - """ - - show_obj = None - - self._log(u"Loading show object for tvdb_id "+str(tvdb_id), logger.DEBUG) - # find the show in the showlist - try: - show_obj = helpers.findCertainShow(sickbeard.showList, tvdb_id) - except exceptions.MultipleShowObjectsException: - raise #TODO: later I'll just log this, for now I want to know about it ASAP - - # if we can't find the show then there's nothing we can really do - if not show_obj: - self._log(("This show (tvdb_id=%d) isn't in your list, you need to add it to SB before post-processing an episode" % tvdb_id), logger.ERROR) - raise exceptions.PostProcessingFailed() - - root_ep = None - for cur_episode in episodes: - episode = int(cur_episode) - - self._log(u"Retrieving episode object for " + str(season) + "x" + str(episode), logger.DEBUG) - - # now that we've figured out which episode this file is just load it manually - try: - curEp = show_obj.getEpisode(season, episode) - except exceptions.EpisodeNotFoundException, e: - self._log(u"Unable to create episode: "+ex(e), logger.DEBUG) - raise exceptions.PostProcessingFailed() - - # associate all the episodes together under a single root episode - if root_ep == None: - root_ep = curEp - root_ep.relatedEps = [] - elif curEp not in root_ep.relatedEps: - root_ep.relatedEps.append(curEp) - - return root_ep - - def _get_quality(self, ep_obj): - """ - Determines the quality of the file that is being post processed, first by checking if it is directly - available in the TVEpisode's status or otherwise by parsing through the data available. - - ep_obj: The TVEpisode object related to the file we are post processing - - Returns: A quality value found in common.Quality - """ - - ep_quality = common.Quality.UNKNOWN - - # 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: - oldStatus, ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable - if ep_quality != common.Quality.UNKNOWN: - self._log(u"The old status had a quality in it, using that: "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) - return ep_quality - - # nzb name is the most reliable if it exists, followed by folder name and lastly file name - name_list = [self.nzb_name, self.folder_name, self.file_name] - - # search all possible names for our new quality, in case the file or dir doesn't have it - for cur_name in name_list: - - # some stuff might be None at this point still - if not cur_name: - continue - - ep_quality = common.Quality.nameQuality(cur_name) - self._log(u"Looking up quality for name "+cur_name+u", got "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) - - # if we find a good one then use it - if ep_quality != common.Quality.UNKNOWN: - logger.log(cur_name+u" looks like it has quality "+common.Quality.qualityStrings[ep_quality]+", using that", logger.DEBUG) - return ep_quality - - # if we didn't get a quality from one of the names above, try assuming from each of the names - ep_quality = common.Quality.assumeQuality(self.file_name) - self._log(u"Guessing quality for name "+self.file_name+u", got "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) - if ep_quality != common.Quality.UNKNOWN: - logger.log(self.file_name+u" looks like it has quality "+common.Quality.qualityStrings[ep_quality]+", using that", logger.DEBUG) - return ep_quality - - return ep_quality - - def _run_extra_scripts(self, ep_obj): - """ - Executes any extra scripts defined in the config. - - ep_obj: The object to use when calling the extra script - """ - for curScriptName in sickbeard.EXTRA_SCRIPTS: - - # generate a safe command line string to execute the script and provide all the parameters - script_cmd = shlex.split(curScriptName) + [ep_obj.location, self.file_path, str(ep_obj.show.tvdbid), str(ep_obj.season), str(ep_obj.episode), str(ep_obj.airdate)] - - # use subprocess to run the command and capture output - self._log(u"Executing command "+str(script_cmd)) - self._log(u"Absolute path to script: "+ek.ek(os.path.abspath, script_cmd[0]), logger.DEBUG) - try: - p = subprocess.Popen(script_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) - out, err = p.communicate() #@UnusedVariable - self._log(u"Script result: "+str(out), logger.DEBUG) - except OSError, e: - self._log(u"Unable to run extra_script: "+ex(e)) - - def _is_priority(self, ep_obj, new_ep_quality): - """ - Determines if the episode is a priority download or not (if it is expected). Episodes which are expected - (snatched) or larger than the existing episode are priority, others are not. - - ep_obj: The TVEpisode object in question - new_ep_quality: The quality of the episode that is being processed - - Returns: True if the episode is priority, False otherwise. - """ - - # if SB downloaded this on purpose then this is a priority download - if self.in_history or ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER: - self._log(u"SB snatched this episode so I'm marking it as priority", logger.DEBUG) - return True - - # if the user downloaded it manually and it's higher quality than the existing episode then it's priority - if new_ep_quality > ep_obj and new_ep_quality != common.Quality.UNKNOWN: - self._log(u"This was manually downloaded but it appears to be better quality than what we have so I'm marking it as priority", logger.DEBUG) - return True - - # if the user downloaded it manually and it appears to be a PROPER/REPACK then it's priority - old_ep_status, old_ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable - if self.is_proper and new_ep_quality >= old_ep_quality: - self._log(u"This was manually downloaded but it appears to be a proper so I'm marking it as priority", logger.DEBUG) - return True - - return False - - def process(self): - """ - Post-process a given file - """ - - self._log(u"Processing " + self.file_path + " (" + str(self.nzb_name) + ")") - - if os.path.isdir(self.file_path): - self._log(u"File " + self.file_path + " seems to be a directory") - return False - for ignore_file in self.IGNORED_FILESTRINGS: - if ignore_file in self.file_path: - self._log(u"File " + self.file_path + " is ignored type, skipping") - return False - # reset per-file stuff - self.in_history = False - - # try to find the file info - (tvdb_id, season, episodes) = self._find_info() - - # if we don't have it then give up - if not tvdb_id or season == None or not episodes: - return False - - # retrieve/create the corresponding TVEpisode objects - ep_obj = self._get_ep_obj(tvdb_id, season, episodes) - - # get the quality of the episode we're processing - new_ep_quality = self._get_quality(ep_obj) - logger.log(u"Quality of the episode we're processing: " + str(new_ep_quality), logger.DEBUG) - - # see if this is a priority download (is it snatched, in history, or PROPER) - priority_download = self._is_priority(ep_obj, new_ep_quality) - self._log(u"Is ep a priority download: " + str(priority_download), logger.DEBUG) - - # set the status of the episodes - for curEp in [ep_obj] + ep_obj.relatedEps: - curEp.status = common.Quality.compositeStatus(common.SNATCHED, new_ep_quality) - - # check for an existing file - existing_file_status = self._checkForExistingFile(ep_obj.location) - - # if it's not priority then we don't want to replace smaller files in case it was a mistake - if not priority_download: - - # if there's an existing file that we don't want to replace stop here - if existing_file_status in (PostProcessor.EXISTS_LARGER, PostProcessor.EXISTS_SAME): - self._log(u"File exists and we are not going to replace it because it's not smaller, quitting post-processing", logger.DEBUG) - return False - elif existing_file_status == PostProcessor.EXISTS_SMALLER: - self._log(u"File exists and is smaller than the new file so I'm going to replace it", logger.DEBUG) - elif existing_file_status != PostProcessor.DOESNT_EXIST: - self._log(u"Unknown existing file status. This should never happen, please log this as a bug.", logger.ERROR) - return False - - # if the file is priority then we're going to replace it even if it exists - else: - self._log(u"This download is marked a priority download so I'm going to replace an existing file if I find one", logger.DEBUG) - - # delete the existing file (and company) - for cur_ep in [ep_obj] + ep_obj.relatedEps: - try: - self._delete(cur_ep.location, associated_files=True) - # clean up any left over folders - if cur_ep.location: - helpers.delete_empty_folders(ek.ek(os.path.dirname, cur_ep.location), keep_dir=ep_obj.show._location) - except (OSError, IOError): - raise exceptions.PostProcessingFailed("Unable to delete the existing files") - - # if the show directory doesn't exist then make it if allowed - if not ek.ek(os.path.isdir, ep_obj.show._location) and sickbeard.CREATE_MISSING_SHOW_DIRS: - self._log(u"Show directory doesn't exist, creating it", logger.DEBUG) - try: - ek.ek(os.mkdir, ep_obj.show._location) - # do the library update for synoindex - notifiers.synoindex_notifier.addFolder(ep_obj.show._location) - - except (OSError, IOError): - raise exceptions.PostProcessingFailed("Unable to create the show directory: " + ep_obj.show._location) - - # get metadata for the show (but not episode because it hasn't been fully processed) - ep_obj.show.writeMetadata(True) - - # update the ep info before we rename so the quality & release name go into the name properly - for cur_ep in [ep_obj] + ep_obj.relatedEps: - with cur_ep.lock: - cur_release_name = None - - # use the best possible representation of the release name - if self.good_results[self.NZB_NAME]: - cur_release_name = self.nzb_name - if cur_release_name.lower().endswith('.nzb'): - cur_release_name = cur_release_name.rpartition('.')[0] - elif self.good_results[self.FOLDER_NAME]: - cur_release_name = self.folder_name - elif self.good_results[self.FILE_NAME]: - cur_release_name = self.file_name - # take the extension off the filename, it's not needed - if '.' in self.file_name: - cur_release_name = self.file_name.rpartition('.')[0] - - if cur_release_name: - self._log("Found release name " + cur_release_name, logger.DEBUG) - cur_ep.release_name = cur_release_name - else: - logger.log("good results: " + repr(self.good_results), logger.DEBUG) - - cur_ep.status = common.Quality.compositeStatus(common.DOWNLOADED, new_ep_quality) - - cur_ep.saveToDB() - - # find the destination folder - try: - proper_path = ep_obj.proper_path() - proper_absolute_path = ek.ek(os.path.join, ep_obj.show.location, proper_path) - - dest_path = ek.ek(os.path.dirname, proper_absolute_path) - except exceptions.ShowDirNotFoundException: - raise exceptions.PostProcessingFailed(u"Unable to post-process an episode if the show dir doesn't exist, quitting") - - self._log(u"Destination folder for this episode: " + dest_path, logger.DEBUG) - - # create any folders we need - helpers.make_dirs(dest_path) - - # figure out the base name of the resulting episode file - if sickbeard.RENAME_EPISODES: - orig_extension = self.file_name.rpartition('.')[-1] - new_base_name = ek.ek(os.path.basename, proper_path) - new_file_name = new_base_name + '.' + orig_extension - - else: - # if we're not renaming then there's no new base name, we'll just use the existing name - new_base_name = None - new_file_name = self.file_name - - with open(self.file_path, 'rb') as fh: - m = hashlib.md5() - while True: - data = fh.read(8192) - if not data: - break - m.update(data) - MD5 = m.hexdigest() - - try: - - path,file=os.path.split(self.file_path) - - if sickbeard.TORRENT_DOWNLOAD_DIR == path: - #Action possible pour les torrent - if sickbeard.PROCESS_METHOD == "copy": - self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) - elif sickbeard.PROCESS_METHOD == "move": - self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) - else: - logger.log(u"Unknown process method: " + str(sickbeard.PROCESS_METHOD), logger.ERROR) - raise exceptions.PostProcessingFailed("Unable to move the files to their new home") - else: - #action pour le reste des fichier - if sickbeard.KEEP_PROCESSED_DIR: - self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) - else: - self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) - - except (OSError, IOError): - raise exceptions.PostProcessingFailed("Unable to move the files to their new home") - - myDB = db.DBConnection() - - ## INSERT MD5 of file - controlMD5 = {"episode_id" : int(ep_obj.tvdbid) } - NewValMD5 = {"filename" : new_base_name , - "md5" : MD5 - } - myDB.upsert("processed_files", NewValMD5, controlMD5) - - - - # put the new location in the database - for cur_ep in [ep_obj] + ep_obj.relatedEps: - with cur_ep.lock: - cur_ep.location = ek.ek(os.path.join, dest_path, new_file_name) - cur_ep.saveToDB() - - # log it to history - history.logDownload(ep_obj, self.file_path, new_ep_quality, self.release_group) - - # download subtitles - if sickbeard.USE_SUBTITLES and ep_obj.show.subtitles: - cur_ep.downloadSubtitles() - - # send notifications - notifiers.notify_download(ep_obj.prettyName()) - - # generate nfo/tbn - ep_obj.createMetaFiles() - ep_obj.saveToDB() - - # 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() - - # 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_library(ep_obj) - - self._run_extra_scripts(ep_obj) - - return True +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import glob +import os +import re +import shlex +import subprocess + +import sickbeard +import hashlib + +from sickbeard import db +from sickbeard import classes +from sickbeard import common +from sickbeard import exceptions +from sickbeard import helpers +from sickbeard import history +from sickbeard import logger +from sickbeard import notifiers +from sickbeard import show_name_helpers +from sickbeard import scene_exceptions + +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex + +from sickbeard.name_parser.parser import NameParser, InvalidNameException + +from lib.tvdb_api import tvdb_api, tvdb_exceptions + +class PostProcessor(object): + """ + A class which will process a media file according to the post processing settings in the config. + """ + + EXISTS_LARGER = 1 + EXISTS_SAME = 2 + EXISTS_SMALLER = 3 + DOESNT_EXIST = 4 + + IGNORED_FILESTRINGS = [ "/.AppleDouble/", ".DS_Store" ] + + NZB_NAME = 1 + FOLDER_NAME = 2 + FILE_NAME = 3 + + def __init__(self, file_path, nzb_name = None): + """ + Creates a new post processor with the given file path and optionally an NZB name. + + file_path: The path to the file to be processed + nzb_name: The name of the NZB which resulted in this file being downloaded (optional) + """ + # absolute path to the folder that is being processed + self.folder_path = ek.ek(os.path.dirname, ek.ek(os.path.abspath, file_path)) + + # full path to file + self.file_path = file_path + + # file name only + self.file_name = ek.ek(os.path.basename, file_path) + + # the name of the folder only + self.folder_name = ek.ek(os.path.basename, self.folder_path) + + # name of the NZB that resulted in this folder + self.nzb_name = nzb_name + + self.in_history = False + self.release_group = None + self.is_proper = False + + self.good_results = {self.NZB_NAME: False, + self.FOLDER_NAME: False, + self.FILE_NAME: False} + + self.log = '' + + def _log(self, message, level=logger.MESSAGE): + """ + A wrapper for the internal logger which also keeps track of messages and saves them to a string for later. + + message: The string to log (unicode) + level: The log level to use (optional) + """ + logger.log(message, level) + self.log += message + '\n' + + def _checkForExistingFile(self, existing_file): + """ + Checks if a file exists already and if it does whether it's bigger or smaller than + the file we are post processing + + existing_file: The file to compare to + + Returns: + DOESNT_EXIST if the file doesn't exist + EXISTS_LARGER if the file exists and is larger than the file we are post processing + EXISTS_SMALLER if the file exists and is smaller than the file we are post processing + EXISTS_SAME if the file exists and is the same size as the file we are post processing + """ + + if not existing_file: + self._log(u"There is no existing file so there's no worries about replacing it", logger.DEBUG) + return PostProcessor.DOESNT_EXIST + + # if the new file exists, return the appropriate code depending on the size + if ek.ek(os.path.isfile, existing_file): + + # see if it's bigger than our old file + if ek.ek(os.path.getsize, existing_file) > ek.ek(os.path.getsize, self.file_path): + self._log(u"File "+existing_file+" is larger than "+self.file_path, logger.DEBUG) + return PostProcessor.EXISTS_LARGER + + elif ek.ek(os.path.getsize, existing_file) == ek.ek(os.path.getsize, self.file_path): + self._log(u"File "+existing_file+" is the same size as "+self.file_path, logger.DEBUG) + return PostProcessor.EXISTS_SAME + + else: + self._log(u"File "+existing_file+" is smaller than "+self.file_path, logger.DEBUG) + return PostProcessor.EXISTS_SMALLER + + else: + self._log(u"File "+existing_file+" doesn't exist so there's no worries about replacing it", logger.DEBUG) + return PostProcessor.DOESNT_EXIST + + def _list_associated_files(self, file_path, subtitles_only=False): + """ + For a given file path searches for files with the same name but different extension and returns their absolute paths + + file_path: The file to check for associated files + + Returns: A list containing all files which are associated to the given file + """ + + if not file_path: + return [] + + file_path_list = [] + dumb_files_list =[] + + base_name = file_path.rpartition('.')[0]+'.' + + # don't strip it all and use cwd by accident + if not base_name: + return [] + + # don't confuse glob with chars we didn't mean to use + base_name = re.sub(r'[\[\]\*\?]', r'[\g<0>]', base_name) + + for associated_file_path in ek.ek(glob.glob, base_name+'*'): + # only add associated to list + if associated_file_path == file_path: + continue + # only list it if the only non-shared part is the extension or if it is a subtitle + + if '.' in associated_file_path[len(base_name):]: + continue + if subtitles_only and not associated_file_path[len(associated_file_path)-3:] in common.subtitleExtensions: + continue + + file_path_list.append(associated_file_path) + + return file_path_list + def _list_dummy_files(self, file_path, oribasename=None,directory=None): + """ + For a given file path searches for dummy files + + Returns: deletes all files which are dummy to the given file + """ + + if not file_path: + return [] + dumb_files_list =[] + if oribasename: + base_name=oribasename + else: + base_name = file_path.rpartition('.')[0]+'.' + + # don't strip it all and use cwd by accident + if not base_name: + return [] + + # don't confuse glob with chars we didn't mean to use + base_name = re.sub(r'[\[\]\*\?]', r'[\g<0>]', base_name) + if directory =="d": + cur_dir=file_path + else: + cur_dir=self.folder_path + ass_files=ek.ek(glob.glob, base_name+'*') + dum_files=ek.ek(glob.glob, cur_dir+'\*') + for dummy_file_path in dum_files: + if os.path.isdir(dummy_file_path): + self._list_dummy_files(dummy_file_path, base_name,"d") + print sickbeard.TORRENT_DOWNLOAD_DIR + print cur_dir + elif dummy_file_path==self.file_path or dummy_file_path[len(dummy_file_path)-3:] in common.mediaExtensions or sickbeard.MOVE_ASSOCIATED_FILES or (sickbeard.TORRENT_DOWNLOAD_DIR in cur_dir and sickbeard.PROCESS_METHOD in ['copy','hardlink','symlink']): + continue + else: + dumb_files_list.append(dummy_file_path) + for cur_file in dumb_files_list: + self._log(u"Deleting file "+cur_file, logger.DEBUG) + if ek.ek(os.path.isfile, cur_file): + ek.ek(os.remove, cur_file) + + return + def _delete(self, file_path, associated_files=False): + """ + Deletes the file and optionally all associated files. + + file_path: The file to delete + associated_files: True to delete all files which differ only by extension, False to leave them + """ + + if not file_path: + return + + # figure out which files we want to delete + file_list = [file_path] + self._list_dummy_files(file_path) + if associated_files: + file_list = file_list + self._list_associated_files(file_path) + + if not file_list: + self._log(u"There were no files associated with " + file_path + ", not deleting anything", logger.DEBUG) + return + + # delete the file and any other files which we want to delete + for cur_file in file_list: + self._log(u"Deleting file "+cur_file, logger.DEBUG) + if ek.ek(os.path.isfile, cur_file): + ek.ek(os.remove, cur_file) + # do the library update for synoindex + notifiers.synoindex_notifier.deleteFile(cur_file) + + def _combined_file_operation (self, file_path, new_path, new_base_name, associated_files=False, action=None, subtitles=False): + """ + Performs a generic operation (move or copy) on a file. Can rename the file as well as change its location, + and optionally move associated files too. + + file_path: The full path of the media file to act on + new_path: Destination path where we want to move/copy the file to + new_base_name: The base filename (no extension) to use during the copy. Use None to keep the same name. + associated_files: Boolean, whether we should copy similarly-named files too + action: function that takes an old path and new path and does an operation with them (move/copy) + """ + + if not action: + self._log(u"Must provide an action for the combined file operation", logger.ERROR) + return + + file_list = [file_path] + self._list_dummy_files(file_path) + if associated_files: + file_list = file_list + self._list_associated_files(file_path) + elif subtitles: + file_list = file_list + self._list_associated_files(file_path, True) + + if not file_list: + self._log(u"There were no files associated with " + file_path + ", not moving anything", logger.DEBUG) + return + + # deal with all files + for cur_file_path in file_list: + + cur_file_name = ek.ek(os.path.basename, cur_file_path) + + # get the extension + cur_extension = cur_file_path.rpartition('.')[-1] + + # check if file have language of subtitles + if cur_extension in common.subtitleExtensions: + cur_lang = cur_file_path.rpartition('.')[0].rpartition('.')[-1] + if cur_lang in sickbeard.SUBTITLES_LANGUAGES: + cur_extension = cur_lang + '.' + cur_extension + + # replace .nfo with .nfo-orig to avoid conflicts + if cur_extension == 'nfo': + cur_extension = 'nfo-orig' + + # If new base name then convert name + if new_base_name: + new_file_name = new_base_name +'.' + cur_extension + # if we're not renaming we still want to change extensions sometimes + else: + new_file_name = helpers.replaceExtension(cur_file_name, cur_extension) + + if sickbeard.SUBTITLES_DIR and cur_extension in common.subtitleExtensions: + subs_new_path = ek.ek(os.path.join, new_path, sickbeard.SUBTITLES_DIR) + dir_exists = helpers.makeDir(subs_new_path) + if not dir_exists: + logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) + else: + helpers.chmodAsParent(subs_new_path) + new_file_path = ek.ek(os.path.join, subs_new_path, new_file_name) + else: + if sickbeard.SUBTITLES_DIR_SUB and cur_extension in common.subtitleExtensions: + subs_new_path = os.path.join(os.path.dirname(file.path),"Subs") + dir_exists = helpers.makeDir(subs_new_path) + if not dir_exists: + logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) + else: + helpers.chmodAsParent(subs_new_path) + new_file_path = ek.ek(os.path.join, subs_new_path, new_file_name) + else : + new_file_path = ek.ek(os.path.join, new_path, new_file_name) + + action(cur_file_path, new_file_path) + + def _move(self, file_path, new_path, new_base_name, associated_files=False, subtitles=False): + """ + file_path: The full path of the media file to move + new_path: Destination path where we want to move the file to + new_base_name: The base filename (no extension) to use during the move. Use None to keep the same name. + associated_files: Boolean, whether we should move similarly-named files too + """ + + def _int_move(cur_file_path, new_file_path): + + self._log(u"Moving file from "+cur_file_path+" to "+new_file_path, logger.DEBUG) + try: + helpers.moveFile(cur_file_path, new_file_path) + helpers.chmodAsParent(new_file_path) + except (IOError, OSError), e: + self._log("Unable to move file "+cur_file_path+" to "+new_file_path+": "+ex(e), logger.ERROR) + raise e + + self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_move, subtitles=subtitles) + + def _copy(self, file_path, new_path, new_base_name, associated_files=False, subtitles=False): + """ + file_path: The full path of the media file to copy + new_path: Destination path where we want to copy the file to + new_base_name: The base filename (no extension) to use during the copy. Use None to keep the same name. + associated_files: Boolean, whether we should copy similarly-named files too + """ + + def _int_copy (cur_file_path, new_file_path): + + self._log(u"Copying file from "+cur_file_path+" to "+new_file_path, logger.DEBUG) + try: + helpers.copyFile(cur_file_path, new_file_path) + helpers.chmodAsParent(new_file_path) + except (IOError, OSError), e: + logger.log("Unable to copy file "+cur_file_path+" to "+new_file_path+": "+ex(e), logger.ERROR) + raise e + + self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_copy, subtitles=subtitles) + + def _hardlink(self, file_path, new_path, new_base_name, associated_files=False): + """ + file_path: The full path of the media file to move + new_path: Destination path where we want to create a hard linked file + new_base_name: The base filename (no extension) to use during the link. Use None to keep the same name. + associated_files: Boolean, whether we should move similarly-named files too + """ + + def _int_hard_link(cur_file_path, new_file_path): + + self._log(u"Hard linking file from " + cur_file_path + " to " + new_file_path, logger.DEBUG) + try: + helpers.hardlinkFile(cur_file_path, new_file_path) + helpers.chmodAsParent(new_file_path) + except (IOError, OSError), e: + self._log("Unable to link file " + cur_file_path + " to " + new_file_path + ": "+ex(e), logger.ERROR) + raise e + self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_hard_link) + + def _moveAndSymlink(self, file_path, new_path, new_base_name, associated_files=False): + """ + file_path: The full path of the media file to move + new_path: Destination path where we want to move the file to create a symbolic link to + new_base_name: The base filename (no extension) to use during the link. Use None to keep the same name. + associated_files: Boolean, whether we should move similarly-named files too + """ + + def _int_move_and_sym_link(cur_file_path, new_file_path): + + self._log(u"Moving then symbolic linking file from " + cur_file_path + " to " + new_file_path, logger.DEBUG) + try: + helpers.moveAndSymlinkFile(cur_file_path, new_file_path) + helpers.chmodAsParent(new_file_path) + except (IOError, OSError), e: + self._log("Unable to link file " + cur_file_path + " to " + new_file_path + ": " + ex(e), logger.ERROR) + raise e + self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_move_and_sym_link) + + def _history_lookup(self): + """ + Look up the NZB name in the history and see if it contains a record for self.nzb_name + + Returns a (tvdb_id, season, []) tuple. The first two may be None if none were found. + """ + + to_return = (None, None, []) + + # 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: + self.in_history = False + return to_return + + # make a list of possible names to use in the search + names = [] + if self.nzb_name: + names.append(self.nzb_name) + if '.' in self.nzb_name: + names.append(self.nzb_name.rpartition(".")[0]) + if self.folder_name: + names.append(self.folder_name) + + myDB = db.DBConnection() + + # search the database for a possible match and return immediately if we find one + for curName in names: + sql_results = myDB.select("SELECT * FROM history WHERE resource LIKE ?", [re.sub("[\.\-\ ]", "_", curName)]) + + if len(sql_results) == 0: + continue + + tvdb_id = int(sql_results[0]["showid"]) + season = int(sql_results[0]["season"]) + + self.in_history = True + to_return = (tvdb_id, season, []) + self._log("Found result in history: "+str(to_return), logger.DEBUG) + + if curName == self.nzb_name: + self.good_results[self.NZB_NAME] = True + elif curName == self.folder_name: + self.good_results[self.FOLDER_NAME] = True + elif curName == self.file_name: + self.good_results[self.FILE_NAME] = True + + return to_return + + self.in_history = False + return to_return + + def _analyze_name(self, name, file=True): + """ + Takes a name and tries to figure out a show, season, and episode from it. + + name: A string which we want to analyze to determine show info from (unicode) + + Returns a (tvdb_id, season, [episodes]) tuple. The first two may be None and episodes may be [] + if none were found. + """ + + logger.log(u"Analyzing name "+repr(name)) + + to_return = (None, None, []) + + if not name: + return to_return + + # parse the name to break it into show name, season, and episode + np = NameParser(file) + parse_result = np.parse(name) + self._log("Parsed "+name+" into "+str(parse_result).decode('utf-8'), logger.DEBUG) + + if parse_result.air_by_date: + season = -1 + episodes = [parse_result.air_date] + else: + season = parse_result.season_number + episodes = parse_result.episode_numbers + + to_return = (None, season, episodes) + + # do a scene reverse-lookup to get a list of all possible names + name_list = show_name_helpers.sceneToNormalShowNames(parse_result.series_name) + + if not name_list: + return (None, season, episodes) + + def _finalize(parse_result): + self.release_group = parse_result.release_group + + # remember whether it's a proper + if parse_result.extra_info: + self.is_proper = re.search('(^|[\. _-])(proper|repack)([\. _-]|$)', parse_result.extra_info, re.I) != None + + # if the result is complete then remember that for later + if parse_result.series_name and parse_result.season_number != None and parse_result.episode_numbers and parse_result.release_group: + test_name = os.path.basename(name) + if test_name == self.nzb_name: + self.good_results[self.NZB_NAME] = True + elif test_name == self.folder_name: + self.good_results[self.FOLDER_NAME] = True + elif test_name == self.file_name: + self.good_results[self.FILE_NAME] = True + else: + logger.log(u"Nothing was good, found "+repr(test_name)+" and wanted either "+repr(self.nzb_name)+", "+repr(self.folder_name)+", or "+repr(self.file_name)) + else: + logger.log("Parse result not suficent(all folowing have to be set). will not save release name", logger.DEBUG) + logger.log("Parse result(series_name): " + str(parse_result.series_name), logger.DEBUG) + logger.log("Parse result(season_number): " + str(parse_result.season_number), logger.DEBUG) + logger.log("Parse result(episode_numbers): " + str(parse_result.episode_numbers), logger.DEBUG) + logger.log("Parse result(release_group): " + str(parse_result.release_group), logger.DEBUG) + + # for each possible interpretation of that scene name + for cur_name in name_list: + self._log(u"Checking scene exceptions for a match on "+cur_name, logger.DEBUG) + scene_id = scene_exceptions.get_scene_exception_by_name(cur_name) + if scene_id: + self._log(u"Scene exception lookup got tvdb id "+str(scene_id)+u", using that", logger.DEBUG) + _finalize(parse_result) + return (scene_id, season, episodes) + + # see if we can find the name directly in the DB, if so use it + for cur_name in name_list: + self._log(u"Looking up "+cur_name+u" in the DB", logger.DEBUG) + db_result = helpers.searchDBForShow(cur_name) + if db_result: + self._log(u"Lookup successful, using tvdb id "+str(db_result[0]), logger.DEBUG) + _finalize(parse_result) + return (int(db_result[0]), season, episodes) + + # see if we can find the name with a TVDB lookup + for cur_name in name_list: + try: + t = tvdb_api.Tvdb(custom_ui=classes.ShowListUI, **sickbeard.TVDB_API_PARMS) + + self._log(u"Looking up name "+cur_name+u" on TVDB", logger.DEBUG) + showObj = t[cur_name] + except (tvdb_exceptions.tvdb_exception): + # if none found, search on all languages + try: + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + ltvdb_api_parms['search_all_languages'] = True + t = tvdb_api.Tvdb(custom_ui=classes.ShowListUI, **ltvdb_api_parms) + + self._log(u"Looking up name "+cur_name+u" in all languages on TVDB", logger.DEBUG) + showObj = t[cur_name] + except (tvdb_exceptions.tvdb_exception, IOError): + pass + + continue + except (IOError): + continue + + self._log(u"Lookup successful, using tvdb id "+str(showObj["id"]), logger.DEBUG) + _finalize(parse_result) + return (int(showObj["id"]), season, episodes) + + _finalize(parse_result) + return to_return + + + def _find_info(self): + """ + For a given file try to find the showid, season, and episode. + """ + + tvdb_id = season = None + episodes = [] + + # try to look up the nzb in history + attempt_list = [self._history_lookup, + + # try to analyze the nzb name + lambda: self._analyze_name(self.nzb_name), + + # try to analyze the file name + lambda: self._analyze_name(self.file_name), + + # try to analyze the dir name + lambda: self._analyze_name(self.folder_name), + + # try to analyze the file+dir names together + lambda: self._analyze_name(self.file_path), + + # try to analyze the dir + file name together as one name + lambda: self._analyze_name(self.folder_name + u' ' + self.file_name) + + ] + + # attempt every possible method to get our info + for cur_attempt in attempt_list: + + try: + (cur_tvdb_id, cur_season, cur_episodes) = cur_attempt() + except InvalidNameException, e: + logger.log(u"Unable to parse, skipping: "+ex(e), logger.DEBUG) + continue + + # if we already did a successful history lookup then keep that tvdb_id value + if cur_tvdb_id and not (self.in_history and tvdb_id): + tvdb_id = cur_tvdb_id + if cur_season != None: + season = cur_season + if cur_episodes: + episodes = cur_episodes + + # for air-by-date shows we need to look up the season/episode from tvdb + if season == -1 and tvdb_id and episodes: + self._log(u"Looks like this is an air-by-date show, attempting to convert the date to season/episode", logger.DEBUG) + + # try to get language set for this show + tvdb_lang = None + try: + showObj = helpers.findCertainShow(sickbeard.showList, tvdb_id) + if(showObj != None): + tvdb_lang = showObj.lang + except exceptions.MultipleShowObjectsException: + raise #TODO: later I'll just log this, for now I want to know about it ASAP + + try: + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if tvdb_lang and not tvdb_lang == 'en': + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + epObj = t[tvdb_id].airedOn(episodes[0])[0] + season = int(epObj["seasonnumber"]) + episodes = [int(epObj["episodenumber"])] + self._log(u"Got season " + str(season) + " episodes " + str(episodes), logger.DEBUG) + except tvdb_exceptions.tvdb_episodenotfound, e: + self._log(u"Unable to find episode with date " + str(episodes[0]) + u" for show " + str(tvdb_id) + u", skipping", logger.DEBUG) + # we don't want to leave dates in the episode list if we couldn't convert them to real episode numbers + episodes = [] + continue + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB: " + ex(e), logger.WARNING) + episodes = [] + continue + + # if there's no season then we can hopefully just use 1 automatically + elif season == None and tvdb_id: + myDB = db.DBConnection() + numseasonsSQlResult = myDB.select("SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0", [tvdb_id]) + if int(numseasonsSQlResult[0][0]) == 1 and season == None: + self._log(u"Don't have a season number, but this show appears to only have 1 season, setting seasonnumber to 1...", logger.DEBUG) + season = 1 + + if tvdb_id and season != None and episodes: + return (tvdb_id, season, episodes) + + return (tvdb_id, season, episodes) + + def _get_ep_obj(self, tvdb_id, season, episodes): + """ + Retrieve the TVEpisode object requested. + + tvdb_id: The TVDBID of the show (int) + season: The season of the episode (int) + episodes: A list of episodes to find (list of ints) + + If the episode(s) can be found then a TVEpisode object with the correct related eps will + be instantiated and returned. If the episode can't be found then None will be returned. + """ + + show_obj = None + + self._log(u"Loading show object for tvdb_id "+str(tvdb_id), logger.DEBUG) + # find the show in the showlist + try: + show_obj = helpers.findCertainShow(sickbeard.showList, tvdb_id) + except exceptions.MultipleShowObjectsException: + raise #TODO: later I'll just log this, for now I want to know about it ASAP + + # if we can't find the show then there's nothing we can really do + if not show_obj: + self._log(("This show (tvdb_id=%d) isn't in your list, you need to add it to SB before post-processing an episode" % tvdb_id), logger.ERROR) + raise exceptions.PostProcessingFailed() + + root_ep = None + for cur_episode in episodes: + episode = int(cur_episode) + + self._log(u"Retrieving episode object for " + str(season) + "x" + str(episode), logger.DEBUG) + + # now that we've figured out which episode this file is just load it manually + try: + curEp = show_obj.getEpisode(season, episode) + except exceptions.EpisodeNotFoundException, e: + self._log(u"Unable to create episode: "+ex(e), logger.DEBUG) + raise exceptions.PostProcessingFailed() + + # associate all the episodes together under a single root episode + if root_ep == None: + root_ep = curEp + root_ep.relatedEps = [] + elif curEp not in root_ep.relatedEps: + root_ep.relatedEps.append(curEp) + + return root_ep + + def _get_quality(self, ep_obj): + """ + Determines the quality of the file that is being post processed, first by checking if it is directly + available in the TVEpisode's status or otherwise by parsing through the data available. + + ep_obj: The TVEpisode object related to the file we are post processing + + Returns: A quality value found in common.Quality + """ + + ep_quality = common.Quality.UNKNOWN + + # 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: + oldStatus, ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable + if ep_quality != common.Quality.UNKNOWN: + self._log(u"The old status had a quality in it, using that: "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) + return ep_quality + + # nzb name is the most reliable if it exists, followed by folder name and lastly file name + name_list = [self.nzb_name, self.folder_name, self.file_name] + + # search all possible names for our new quality, in case the file or dir doesn't have it + for cur_name in name_list: + + # some stuff might be None at this point still + if not cur_name: + continue + + ep_quality = common.Quality.nameQuality(cur_name) + self._log(u"Looking up quality for name "+cur_name+u", got "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) + + # if we find a good one then use it + if ep_quality != common.Quality.UNKNOWN: + logger.log(cur_name+u" looks like it has quality "+common.Quality.qualityStrings[ep_quality]+", using that", logger.DEBUG) + return ep_quality + + # if we didn't get a quality from one of the names above, try assuming from each of the names + ep_quality = common.Quality.assumeQuality(self.file_name) + self._log(u"Guessing quality for name "+self.file_name+u", got "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) + if ep_quality != common.Quality.UNKNOWN: + logger.log(self.file_name+u" looks like it has quality "+common.Quality.qualityStrings[ep_quality]+", using that", logger.DEBUG) + return ep_quality + + return ep_quality + + def _run_extra_scripts(self, ep_obj): + """ + Executes any extra scripts defined in the config. + + ep_obj: The object to use when calling the extra script + """ + for curScriptName in sickbeard.EXTRA_SCRIPTS: + + # generate a safe command line string to execute the script and provide all the parameters + script_cmd = shlex.split(curScriptName) + [ep_obj.location, self.file_path, str(ep_obj.show.tvdbid), str(ep_obj.season), str(ep_obj.episode), str(ep_obj.airdate)] + + # use subprocess to run the command and capture output + self._log(u"Executing command "+str(script_cmd)) + self._log(u"Absolute path to script: "+ek.ek(os.path.abspath, script_cmd[0]), logger.DEBUG) + try: + p = subprocess.Popen(script_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) + out, err = p.communicate() #@UnusedVariable + self._log(u"Script result: "+str(out), logger.DEBUG) + except OSError, e: + self._log(u"Unable to run extra_script: "+ex(e)) + + def _is_priority(self, ep_obj, new_ep_quality): + """ + Determines if the episode is a priority download or not (if it is expected). Episodes which are expected + (snatched) or larger than the existing episode are priority, others are not. + + ep_obj: The TVEpisode object in question + new_ep_quality: The quality of the episode that is being processed + + Returns: True if the episode is priority, False otherwise. + """ + + # if SB downloaded this on purpose then this is a priority download + if self.in_history or ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER: + self._log(u"SB snatched this episode so I'm marking it as priority", logger.DEBUG) + return True + + # if the user downloaded it manually and it's higher quality than the existing episode then it's priority + if new_ep_quality > ep_obj and new_ep_quality != common.Quality.UNKNOWN: + self._log(u"This was manually downloaded but it appears to be better quality than what we have so I'm marking it as priority", logger.DEBUG) + return True + + # if the user downloaded it manually and it appears to be a PROPER/REPACK then it's priority + old_ep_status, old_ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable + if self.is_proper and new_ep_quality >= old_ep_quality: + self._log(u"This was manually downloaded but it appears to be a proper so I'm marking it as priority", logger.DEBUG) + return True + + return False + + def process(self): + """ + Post-process a given file + """ + + self._log(u"Processing " + self.file_path + " (" + str(self.nzb_name) + ")") + + if os.path.isdir(self.file_path): + self._log(u"File " + self.file_path + " seems to be a directory") + return False + for ignore_file in self.IGNORED_FILESTRINGS: + if ignore_file in self.file_path: + self._log(u"File " + self.file_path + " is ignored type, skipping") + return False + # reset per-file stuff + self.in_history = False + + # try to find the file info + (tvdb_id, season, episodes) = self._find_info() + + # if we don't have it then give up + if not tvdb_id or season == None or not episodes: + return False + + # retrieve/create the corresponding TVEpisode objects + ep_obj = self._get_ep_obj(tvdb_id, season, episodes) + + # get the quality of the episode we're processing + new_ep_quality = self._get_quality(ep_obj) + logger.log(u"Quality of the episode we're processing: " + str(new_ep_quality), logger.DEBUG) + + # see if this is a priority download (is it snatched, in history, or PROPER) + priority_download = self._is_priority(ep_obj, new_ep_quality) + self._log(u"Is ep a priority download: " + str(priority_download), logger.DEBUG) + + # set the status of the episodes + for curEp in [ep_obj] + ep_obj.relatedEps: + curEp.status = common.Quality.compositeStatus(common.SNATCHED, new_ep_quality) + + # check for an existing file + existing_file_status = self._checkForExistingFile(ep_obj.location) + + # if it's not priority then we don't want to replace smaller files in case it was a mistake + if not priority_download: + + # if there's an existing file that we don't want to replace stop here + if existing_file_status in (PostProcessor.EXISTS_LARGER, PostProcessor.EXISTS_SAME): + self._log(u"File exists and we are not going to replace it because it's not smaller, quitting post-processing", logger.DEBUG) + return False + elif existing_file_status == PostProcessor.EXISTS_SMALLER: + self._log(u"File exists and is smaller than the new file so I'm going to replace it", logger.DEBUG) + elif existing_file_status != PostProcessor.DOESNT_EXIST: + self._log(u"Unknown existing file status. This should never happen, please log this as a bug.", logger.ERROR) + return False + + # if the file is priority then we're going to replace it even if it exists + else: + self._log(u"This download is marked a priority download so I'm going to replace an existing file if I find one", logger.DEBUG) + + # delete the existing file (and company) + for cur_ep in [ep_obj] + ep_obj.relatedEps: + try: + self._delete(cur_ep.location, associated_files=True) + # clean up any left over folders + if cur_ep.location: + helpers.delete_empty_folders(ek.ek(os.path.dirname, cur_ep.location), keep_dir=ep_obj.show._location) + except (OSError, IOError): + raise exceptions.PostProcessingFailed("Unable to delete the existing files") + + # if the show directory doesn't exist then make it if allowed + if not ek.ek(os.path.isdir, ep_obj.show._location) and sickbeard.CREATE_MISSING_SHOW_DIRS: + self._log(u"Show directory doesn't exist, creating it", logger.DEBUG) + try: + ek.ek(os.mkdir, ep_obj.show._location) + # do the library update for synoindex + notifiers.synoindex_notifier.addFolder(ep_obj.show._location) + + except (OSError, IOError): + raise exceptions.PostProcessingFailed("Unable to create the show directory: " + ep_obj.show._location) + + # get metadata for the show (but not episode because it hasn't been fully processed) + ep_obj.show.writeMetadata(True) + + # update the ep info before we rename so the quality & release name go into the name properly + for cur_ep in [ep_obj] + ep_obj.relatedEps: + with cur_ep.lock: + cur_release_name = None + + # use the best possible representation of the release name + if self.good_results[self.NZB_NAME]: + cur_release_name = self.nzb_name + if cur_release_name.lower().endswith('.nzb'): + cur_release_name = cur_release_name.rpartition('.')[0] + elif self.good_results[self.FOLDER_NAME]: + cur_release_name = self.folder_name + elif self.good_results[self.FILE_NAME]: + cur_release_name = self.file_name + # take the extension off the filename, it's not needed + if '.' in self.file_name: + cur_release_name = self.file_name.rpartition('.')[0] + + if cur_release_name: + self._log("Found release name " + cur_release_name, logger.DEBUG) + cur_ep.release_name = cur_release_name + else: + logger.log("good results: " + repr(self.good_results), logger.DEBUG) + + cur_ep.status = common.Quality.compositeStatus(common.DOWNLOADED, new_ep_quality) + + cur_ep.saveToDB() + + # find the destination folder + try: + proper_path = ep_obj.proper_path() + proper_absolute_path = ek.ek(os.path.join, ep_obj.show.location, proper_path) + + dest_path = ek.ek(os.path.dirname, proper_absolute_path) + except exceptions.ShowDirNotFoundException: + raise exceptions.PostProcessingFailed(u"Unable to post-process an episode if the show dir doesn't exist, quitting") + + self._log(u"Destination folder for this episode: " + dest_path, logger.DEBUG) + + # create any folders we need + helpers.make_dirs(dest_path) + + # figure out the base name of the resulting episode file + if sickbeard.RENAME_EPISODES: + orig_extension = self.file_name.rpartition('.')[-1] + new_base_name = ek.ek(os.path.basename, proper_path) + new_file_name = new_base_name + '.' + orig_extension + + else: + # if we're not renaming then there's no new base name, we'll just use the existing name + new_base_name = None + new_file_name = self.file_name + + with open(self.file_path, 'rb') as fh: + m = hashlib.md5() + while True: + data = fh.read(8192) + if not data: + break + m.update(data) + MD5 = m.hexdigest() + + try: + + path,file=os.path.split(self.file_path) + + if sickbeard.TORRENT_DOWNLOAD_DIR == path: + #Action possible pour les torrent + if sickbeard.PROCESS_METHOD == "copy": + self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + elif sickbeard.PROCESS_METHOD == "move": + self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + elif sickbeard.PROCESS_METHOD == "hardlink": + self._hardlink(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + elif sickbeard.PROCESS_METHOD == "symlink": + self._moveAndSymlink(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + else: + logger.log(u"Unknown process method: " + str(sickbeard.PROCESS_METHOD), logger.ERROR) + raise exceptions.PostProcessingFailed("Unable to move the files to their new home") + else: + #action pour le reste des fichier + if sickbeard.KEEP_PROCESSED_DIR: + self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + else: + self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + + except (OSError, IOError): + raise exceptions.PostProcessingFailed("Unable to move the files to their new home") + + myDB = db.DBConnection() + + ## INSERT MD5 of file + controlMD5 = {"episode_id" : int(ep_obj.tvdbid) } + NewValMD5 = {"filename" : new_base_name , + "md5" : MD5 + } + myDB.upsert("processed_files", NewValMD5, controlMD5) + + + + # put the new location in the database + for cur_ep in [ep_obj] + ep_obj.relatedEps: + with cur_ep.lock: + cur_ep.location = ek.ek(os.path.join, dest_path, new_file_name) + cur_ep.saveToDB() + + # log it to history + history.logDownload(ep_obj, self.file_path, new_ep_quality, self.release_group) + + # download subtitles + if sickbeard.USE_SUBTITLES and ep_obj.show.subtitles: + cur_ep.downloadSubtitles() + + # send notifications + notifiers.notify_download(ep_obj.prettyName()) + + # generate nfo/tbn + ep_obj.createMetaFiles() + ep_obj.saveToDB() + + # 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() + + # 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_library(ep_obj) + + self._run_extra_scripts(ep_obj) + + return True diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py index d879b4fdf9c305fd207e7744f52322ef13a58c60..5639041cc039c757078b031f00114f3900e29bd1 100644 --- a/sickbeard/processTV.py +++ b/sickbeard/processTV.py @@ -1,168 +1,168 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import os -import shutil -import hashlib - -import sickbeard -from sickbeard import postProcessor -from sickbeard import db, helpers, exceptions - -from sickbeard import encodingKludge as ek -from sickbeard.exceptions import ex - -from sickbeard import logger - -def logHelper (logMessage, logLevel=logger.MESSAGE): - logger.log(logMessage, logLevel) - return logMessage + u"\n" - -def processDir (dirName, nzbName=None, recurse=False): - """ - Scans through the files in dirName and processes whatever media files it finds - - dirName: The folder name to look in - nzbName: The NZB name which resulted in this folder being downloaded - recurse: Boolean for whether we should descend into subfolders or not - """ - - returnStr = '' - - returnStr += logHelper(u"Processing folder "+dirName, logger.DEBUG) - - # if they passed us a real dir then assume it's the one we want - if ek.ek(os.path.isdir, dirName): - dirName = ek.ek(os.path.realpath, dirName) - - # if they've got a download dir configured then use it - elif sickbeard.TV_DOWNLOAD_DIR and ek.ek(os.path.isdir, sickbeard.TV_DOWNLOAD_DIR) \ - and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR): - dirName = ek.ek(os.path.join, sickbeard.TV_DOWNLOAD_DIR, ek.ek(os.path.abspath, dirName).split(os.path.sep)[-1]) - returnStr += logHelper(u"Trying to use folder "+dirName, logger.DEBUG) - - # if we didn't find a real dir then quit - if not ek.ek(os.path.isdir, dirName): - returnStr += logHelper(u"Unable to figure out what folder to process. If your downloader and Sick Beard aren't on the same PC make sure you fill out your TV download dir in the config.", logger.DEBUG) - return returnStr - - # TODO: check if it's failed and deal with it if it is - if ek.ek(os.path.basename, dirName).startswith('_FAILED_'): - returnStr += logHelper(u"The directory name indicates it failed to extract, cancelling", logger.DEBUG) - return returnStr - elif ek.ek(os.path.basename, dirName).startswith('_UNDERSIZED_'): - returnStr += logHelper(u"The directory name indicates that it was previously rejected for being undersized, cancelling", logger.DEBUG) - return returnStr - elif ek.ek(os.path.basename, dirName).startswith('_UNPACK_'): - returnStr += logHelper(u"The directory name indicates that this release is in the process of being unpacked, skipping", logger.DEBUG) - return returnStr - - # make sure the dir isn't inside a show dir - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_shows") - for sqlShow in sqlResults: - if dirName.lower().startswith(ek.ek(os.path.realpath, sqlShow["location"]).lower()+os.sep) or dirName.lower() == ek.ek(os.path.realpath, sqlShow["location"]).lower(): - returnStr += logHelper(u"You're trying to post process an episode that's already been moved to its show dir", logger.ERROR) - return returnStr - - fileList = ek.ek(os.listdir, dirName) - - # split the list into video files and folders - folders = filter(lambda x: ek.ek(os.path.isdir, ek.ek(os.path.join, dirName, x)), fileList) - videoFiles = filter(helpers.isMediaFile, fileList) - - # recursively process all the folders - for curFolder in folders: - returnStr += logHelper(u"Recursively processing a folder: "+curFolder, logger.DEBUG) - returnStr += processDir(ek.ek(os.path.join, dirName, curFolder), recurse=True) - - remainingFolders = filter(lambda x: ek.ek(os.path.isdir, ek.ek(os.path.join, dirName, x)), fileList) - - # If nzbName is set and there's more than one videofile in the folder, files will be lost (overwritten). - if nzbName != None and len(videoFiles) >= 2: - nzbName = None - - # process any files in the dir - for cur_video_file_path in videoFiles: - - cur_video_file_path = ek.ek(os.path.join, dirName, cur_video_file_path) - - # IF VIDEO_FILE ALREADY PROCESS THEN CONTINUE - # TODO - - myDB = db.DBConnection() - - with open(cur_video_file_path, 'rb') as fh: - m = hashlib.md5() - while True: - data = fh.read(8192) - if not data: - break - m.update(data) - MD5 = m.hexdigest() - - logger.log("MD5 search : " + MD5, logger.DEBUG) - - sqlResults = myDB.select("select * from processed_files where md5 = \"" + MD5 + "\"") - - process_file = True - - for sqlProcess in sqlResults: - if sqlProcess["md5"] == MD5: - logger.log("File " + cur_video_file_path + " already processed for " + sqlProcess["filename"]) - process_file = False - - if process_file: - try: - processor = postProcessor.PostProcessor(cur_video_file_path, nzbName) - process_result = processor.process() - process_fail_message = "" - except exceptions.PostProcessingFailed, e: - process_result = False - process_fail_message = ex(e) - - returnStr += processor.log - - # as long as the postprocessing was successful delete the old folder unless the config wants us not to - if process_result: - - if len(videoFiles) == 1 \ - and ( ( not sickbeard.KEEP_PROCESSED_DIR and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR) ) \ - or ( sickbeard.PROCESS_METHOD == "move" and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TORRENT_DOWNLOAD_DIR) ) ) \ - and len(remainingFolders) == 0: - - returnStr += logHelper(u"Deleting folder " + dirName, logger.DEBUG) - - try: - shutil.rmtree(dirName) - except (OSError, IOError), e: - returnStr += logHelper(u"Warning: unable to remove the folder " + dirName + ": " + ex(e), logger.WARNING) - - returnStr += logHelper(u"Processing succeeded for "+cur_video_file_path) - - else: - returnStr += logHelper(u"Processing failed for "+cur_video_file_path+": "+process_fail_message, logger.WARNING) - if sickbeard.TV_DOWNLOAD_DIR !="": - for i in range(1,5): - helpers.del_empty_dirs(sickbeard.TV_DOWNLOAD_DIR) - if sickbeard.TORRENT_DOWNLOAD_DIR !="": - for i in range(1,5): - helpers.del_empty_dirs(sickbeard.TORRENT_DOWNLOAD_DIR) - return returnStr +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import os +import shutil +import hashlib + +import sickbeard +from sickbeard import postProcessor +from sickbeard import db, helpers, exceptions + +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex + +from sickbeard import logger + +def logHelper (logMessage, logLevel=logger.MESSAGE): + logger.log(logMessage, logLevel) + return logMessage + u"\n" + +def processDir (dirName, nzbName=None, recurse=False): + """ + Scans through the files in dirName and processes whatever media files it finds + + dirName: The folder name to look in + nzbName: The NZB name which resulted in this folder being downloaded + recurse: Boolean for whether we should descend into subfolders or not + """ + + returnStr = '' + + returnStr += logHelper(u"Processing folder "+dirName, logger.DEBUG) + + # if they passed us a real dir then assume it's the one we want + if ek.ek(os.path.isdir, dirName): + dirName = ek.ek(os.path.realpath, dirName) + + # if they've got a download dir configured then use it + elif sickbeard.TV_DOWNLOAD_DIR and ek.ek(os.path.isdir, sickbeard.TV_DOWNLOAD_DIR) \ + and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR): + dirName = ek.ek(os.path.join, sickbeard.TV_DOWNLOAD_DIR, ek.ek(os.path.abspath, dirName).split(os.path.sep)[-1]) + returnStr += logHelper(u"Trying to use folder "+dirName, logger.DEBUG) + + # if we didn't find a real dir then quit + if not ek.ek(os.path.isdir, dirName): + returnStr += logHelper(u"Unable to figure out what folder to process. If your downloader and Sick Beard aren't on the same PC make sure you fill out your TV download dir in the config.", logger.DEBUG) + return returnStr + + # TODO: check if it's failed and deal with it if it is + if ek.ek(os.path.basename, dirName).startswith('_FAILED_'): + returnStr += logHelper(u"The directory name indicates it failed to extract, cancelling", logger.DEBUG) + return returnStr + elif ek.ek(os.path.basename, dirName).startswith('_UNDERSIZED_'): + returnStr += logHelper(u"The directory name indicates that it was previously rejected for being undersized, cancelling", logger.DEBUG) + return returnStr + elif ek.ek(os.path.basename, dirName).startswith('_UNPACK_'): + returnStr += logHelper(u"The directory name indicates that this release is in the process of being unpacked, skipping", logger.DEBUG) + return returnStr + + # make sure the dir isn't inside a show dir + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT * FROM tv_shows") + for sqlShow in sqlResults: + if dirName.lower().startswith(ek.ek(os.path.realpath, sqlShow["location"]).lower()+os.sep) or dirName.lower() == ek.ek(os.path.realpath, sqlShow["location"]).lower(): + returnStr += logHelper(u"You're trying to post process an episode that's already been moved to its show dir", logger.ERROR) + return returnStr + + fileList = ek.ek(os.listdir, dirName) + + # split the list into video files and folders + folders = filter(lambda x: ek.ek(os.path.isdir, ek.ek(os.path.join, dirName, x)), fileList) + videoFiles = filter(helpers.isMediaFile, fileList) + + # recursively process all the folders + for curFolder in folders: + returnStr += logHelper(u"Recursively processing a folder: "+curFolder, logger.DEBUG) + returnStr += processDir(ek.ek(os.path.join, dirName, curFolder), recurse=True) + + remainingFolders = filter(lambda x: ek.ek(os.path.isdir, ek.ek(os.path.join, dirName, x)), fileList) + + # If nzbName is set and there's more than one videofile in the folder, files will be lost (overwritten). + if nzbName != None and len(videoFiles) >= 2: + nzbName = None + + # process any files in the dir + for cur_video_file_path in videoFiles: + + cur_video_file_path = ek.ek(os.path.join, dirName, cur_video_file_path) + + # IF VIDEO_FILE ALREADY PROCESS THEN CONTINUE + # TODO + + myDB = db.DBConnection() + + with open(cur_video_file_path, 'rb') as fh: + m = hashlib.md5() + while True: + data = fh.read(8192) + if not data: + break + m.update(data) + MD5 = m.hexdigest() + + logger.log("MD5 search : " + MD5, logger.DEBUG) + + sqlResults = myDB.select("select * from processed_files where md5 = \"" + MD5 + "\"") + + process_file = True + + for sqlProcess in sqlResults: + if sqlProcess["md5"] == MD5: + logger.log("File " + cur_video_file_path + " already processed for ") + process_file = False + + if process_file: + try: + processor = postProcessor.PostProcessor(cur_video_file_path, nzbName) + process_result = processor.process() + process_fail_message = "" + except exceptions.PostProcessingFailed, e: + process_result = False + process_fail_message = ex(e) + + returnStr += processor.log + + # as long as the postprocessing was successful delete the old folder unless the config wants us not to + if process_result: + + if len(videoFiles) == 1 \ + and ( ( not sickbeard.KEEP_PROCESSED_DIR and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR) ) \ + or ( sickbeard.PROCESS_METHOD == "move" and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TORRENT_DOWNLOAD_DIR) ) ) \ + and len(remainingFolders) == 0: + + returnStr += logHelper(u"Deleting folder " + dirName, logger.DEBUG) + + try: + shutil.rmtree(dirName) + except (OSError, IOError), e: + returnStr += logHelper(u"Warning: unable to remove the folder " + dirName + ": " + ex(e), logger.WARNING) + + returnStr += logHelper(u"Processing succeeded for "+cur_video_file_path) + + else: + returnStr += logHelper(u"Processing failed for "+cur_video_file_path+": "+process_fail_message, logger.WARNING) + if sickbeard.TV_DOWNLOAD_DIR !="": + for i in range(1,5): + helpers.del_empty_dirs(sickbeard.TV_DOWNLOAD_DIR) + if sickbeard.TORRENT_DOWNLOAD_DIR !="": + for i in range(1,5): + helpers.del_empty_dirs(sickbeard.TORRENT_DOWNLOAD_DIR) + return returnStr diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 2cdb0ada54e6b549c06b1cc7198d8bb14cbceb3c..bf7199031bdecbdf43e462e4a78fcdc35d0bc141 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -24,7 +24,7 @@ import threading import re import glob import traceback -import hashlib +import hashlib import sickbeard @@ -968,7 +968,7 @@ class TVShow(object): myDB.upsert("tv_shows", newValueDict, controlValueDict) - + if self.imdbid: controlValueDict = {"tvdb_id": self.tvdbid} newValueDict = self.imdb_info @@ -1618,12 +1618,12 @@ class TVEpisode(object): "season": self.season, "episode": self.episode} - - + + # use a custom update/insert method to get the data into the DB myDB.upsert("tv_episodes", newValueDict, controlValueDict) - + def fullPath (self): if self.location == None or self.location == "": return None diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c041c3ce3092de15dd78a820121051496d59707b..2570ec371a02bdcd7baab785a972c55c72b4e16c 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -768,7 +768,7 @@ class History: { 'title': 'Clear History', 'path': 'history/clearHistory' }, { 'title': 'Trim History', 'path': 'history/trimHistory' }, { 'title': 'Trunc Episode Links', 'path': 'history/truncEplinks' }, - { 'title': 'Trunc Episode List Processed', 'path': 'history/truncEpListProc' }, + { 'title': 'Trunc Episode List Processed', 'path': 'history/truncEpListProc' }, ] return _munge(t) @@ -802,15 +802,15 @@ class History: ui.notifications.message('All Episode Links Removed', messnum) redirect("/history") - @cherrypy.expose - def truncEpListProc(self): - myDB = db.DBConnection() - nbep=myDB.select("SELECT count(*) from processed_files") - myDB.action("DELETE FROM processed_files WHERE 1=1") - messnum = str(nbep[0][0]) + ' record for file processed delete' - ui.notifications.message('Clear list of file processed', messnum) - redirect("/history") - + @cherrypy.expose + def truncEpListProc(self): + myDB = db.DBConnection() + nbep=myDB.select("SELECT count(*) from processed_files") + myDB.action("DELETE FROM processed_files WHERE 1=1") + messnum = str(nbep[0][0]) + ' record for file processed delete' + ui.notifications.message('Clear list of file processed', messnum) + redirect("/history") + ConfigMenu = [ { 'title': 'General', 'path': 'config/general/' }, @@ -1096,7 +1096,7 @@ class ConfigPostProcessing: @cherrypy.expose def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, xbmc_data=None, xbmc__frodo__data=None, mediabrowser_data=None, synology_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, - use_banner=None, keep_processed_dir=None, process_method=None, process_automatically=None, process_automatically_torrent=None, rename_episodes=None, + use_banner=None, keep_processed_dir=None, process_method=None, process_automatically=None, process_automatically_torrent=None, rename_episodes=None, move_associated_files=None, tv_download_dir=None, torrent_download_dir=None, naming_custom_abd=None, naming_abd_pattern=None): results = [] @@ -1145,7 +1145,7 @@ class ConfigPostProcessing: sickbeard.PROCESS_AUTOMATICALLY = process_automatically sickbeard.PROCESS_AUTOMATICALLY_TORRENT = process_automatically_torrent sickbeard.KEEP_PROCESSED_DIR = keep_processed_dir - sickbeard.PROCESS_METHOD = process_method + sickbeard.PROCESS_METHOD = process_method sickbeard.RENAME_EPISODES = rename_episodes sickbeard.MOVE_ASSOCIATED_FILES = move_associated_files sickbeard.NAMING_CUSTOM_ABD = naming_custom_abd @@ -3516,7 +3516,7 @@ class WebInterface: sickbeard.TOGGLE_SEARCH= search redirect("/home") - + @cherrypy.expose def toggleDisplayShowSpecials(self, show):