diff --git a/SickBeard.py b/SickBeard.py index a5456a42a30ec8709ab35008ff577b49cda04dbd..a3a32124034f4a00d927cec5186b43a26948bcdf 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -26,10 +26,14 @@ codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else No import time import signal import sys -import shutil import subprocess import traceback +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + if sys.version_info < (2, 6): print "Sorry, requires Python 2.6 or 2.7." sys.exit(1) diff --git a/gui/slick/images/providers/binsearch.png b/gui/slick/images/providers/binsearch.png new file mode 100644 index 0000000000000000000000000000000000000000..890ff858119a95e9841fc9bb8f38bd5989d850f8 Binary files /dev/null and b/gui/slick/images/providers/binsearch.png differ diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl index a42dad039b965463c989f1eaa042c9b0efa1dd07..3a45fbe84ba014f784fc0b2dce5ce1bce3096a78 100644 --- a/gui/slick/interfaces/default/config_general.tmpl +++ b/gui/slick/interfaces/default/config_general.tmpl @@ -83,6 +83,16 @@ </label> </div> + <div class="field-pair"> + <label for="update_shows_on_snatch"> + <span class="component-title">Update shows on snatch</span> + <span class="component-desc"> + <input type="checkbox" name="update_shows_on_snatch" id="update_shows_on_snatch" #if $sickbeard.UPDATE_SHOWS_ON_SNATCH then 'checked="checked"' else ''#/> + <p>with information such as next air dates, show ended, etc.</p> + </span> + </label> + </div> + <div class="field-pair"> <span class="component-title">Send to trash for actions</span> <span class="component-desc"> @@ -334,7 +344,7 @@ <span class="component-title">Browser video player</span> <span class="component-desc"> <input type="checkbox" name="play_videos" id="play_videos" #if $sickbeard.PLAY_VIDEOS then 'checked="checked"' else ''#/> - <p>play video files from display show page<?p> + <p>play video files from display show page</p> </span> </label> </div> @@ -568,14 +578,29 @@ <span class="component-title">Branch version:</span> <span class="component-desc"> <select id="branchVersion" class="form-control form-control-inline input-sm pull-left"> - #if $sickbeard.versionCheckScheduler.action.list_remote_branches() - #for $cur_branch in $sickbeard.versionCheckScheduler.action.list_remote_branches(): - <option value="$cur_branch" #if $cur_branch == $sickbeard.BRANCH then 'selected="selected"' else ''#>$cur_branch</option> - #end for + #set $gh_branch = $sickbeard.versionCheckScheduler.action.list_remote_branches() + #if $gh_branch: + #for $cur_branch in $gh_branch: + #if $sickbeard.GIT_USERNAME and $sickbeard.GIT_PASSWORD and $sickbeard.DEVELOPER == 1 + <option value="$cur_branch" #if $cur_branch == $sickbeard.BRANCH then 'selected="selected"' else ''#>$cur_branch</option> + #else if $sickbeard.GIT_USERNAME and $sickbeard.GIT_PASSWORD and $cur_branch in ['master', 'develop'] + <option value="$cur_branch" #if $cur_branch == $sickbeard.BRANCH then 'selected="selected"' else ''#>$cur_branch</option> + #else if $cur_branch == 'master' + <option value="$cur_branch" #if $cur_branch == $sickbeard.BRANCH then 'selected="selected"' else ''#>$cur_branch</option> + #end if + #end for #end if </select> - <input class="btn btn-inline" style="margin-left: 6px;" type="button" id="branchCheckout" value="Checkout Branch"> - <div class="clear-left"><p>select branch to use (restart required)</p></div> + #if not $gh_branch + <input class="btn btn-inline" style="margin-left: 6px;" type="button" id="branchCheckout" value="Checkout Branch" disabled> + #else + <input class="btn btn-inline" style="margin-left: 6px;" type="button" id="branchCheckout" value="Checkout Branch"> + #end if + #if not $gh_branch + <div class="clear-left" style="color:#FF0000"><p>Error: No branches found.</p></div> + #else + <div class="clear-left"><p>select branch to use (restart required)</p></div> + #end if </span> </label> </div> @@ -630,11 +655,11 @@ </label> </div> - <div class="field-pair"> + <div class="field-pair" hidden> <label for="git_autoissues"> <span class="component-title">Git auto-issues submit</span> <span class="component-desc"> - <input type="checkbox" name="git_autoissues" id="git_autoissues" #if True == $sickbeard.GIT_AUTOISSUES then 'checked="checked"' else ''#/> + <input type="checkbox" name="git_autoissues" id="git_autoissues" #if True == $sickbeard.GIT_AUTOISSUES then 'checked="checked"' else ''# disable/> <p>automatically submit bug/issue reports to our issue tracker when errors are logged</p> </span> </label> diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index 5d367cb8adc0326f5c8a3532275a65a788d01164..2a397300f72f902e27fb3b25356f1b43e667b306 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -1444,15 +1444,16 @@ </label> </div> <div class="field-pair"> - <label for="trakt_use_watchlist"> - <span class="component-title">Use watchlist:</span> + <label for="trakt_sync_watchlist"> + <span class="component-title">Sync watchlist:</span> <span class="component-desc"> - <input type="checkbox" class="enabler" name="trakt_use_watchlist" id="trakt_use_watchlist" #if $sickbeard.TRAKT_USE_WATCHLIST then "checked=\"checked\"" else ""# /> - <p>get new shows from your trakt watchlist.</p> + <input type="checkbox" class="enabler" name="trakt_sync_watchlist" id="trakt_sync_watchlist" #if $sickbeard.TRAKT_SYNC_WATCHLIST then "checked=\"checked\"" else ""# /> + <p>sync your SickRage show watchlist with your trakt show watchlist (either Show and Episode).</p> + <p>Episode will be added on watch list when wanted or snatched and will be removed when downloaded </p> </span> </label> </div> - <div id="content_trakt_use_watchlist"> + <div id="content_trakt_sync_watchlist"> <div class="field-pair"> <label for="trakt_method_add"> <span class="component-title">Watchlist add method:</span> diff --git a/gui/slick/interfaces/default/config_providers.tmpl b/gui/slick/interfaces/default/config_providers.tmpl index ef5bc13c72fb452eb4c187372b50d4adf118bd38..88aa3e084d452733b35f9da3223c7cc4f23abdd5 100644 --- a/gui/slick/interfaces/default/config_providers.tmpl +++ b/gui/slick/interfaces/default/config_providers.tmpl @@ -37,7 +37,7 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <!-- \$(document).ready(function(){ #for $curTorrentRssProvider in $sickbeard.torrentRssProviderList: - \$(this).addTorrentRssProvider('$curTorrentRssProvider.getID()', '$curTorrentRssProvider.name', '$curTorrentRssProvider.url', '$curTorrentRssProvider.cookies'); + \$(this).addTorrentRssProvider('$curTorrentRssProvider.getID()', '$curTorrentRssProvider.name', '$curTorrentRssProvider.url', '$curTorrentRssProvider.cookies', '$curTorrentRssProvider.titleTAG'); #end for }); //--> @@ -688,7 +688,16 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <span class="component-desc">eg. uid=xx;pass=yy</span> </label> </div> - + <div class="field-pair"> + <label for="torrentrss_titleTAG"> + <span class="component-title">Search element:</span> + <input type="text" id="torrentrss_titleTAG" class="form-control input-sm input200" value="title"/> + </label> + <label> + <span class="component-title"> </span> + <span class="component-desc">eg: title</span> + </label> + </div> <div id="torrentrss_add_div"> <input type="button" class="btn torrentrss_save" id="torrentrss_add" value="Add" /> </div> diff --git a/gui/slick/interfaces/default/home.tmpl b/gui/slick/interfaces/default/home.tmpl index a2d29e07c31ec6d3b0f9ae2aa84eed86a15fd4d3..5a406c25be462d8babd382a15e61d510f1faf16b 100644 --- a/gui/slick/interfaces/default/home.tmpl +++ b/gui/slick/interfaces/default/home.tmpl @@ -384,7 +384,13 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name)) <div class="show-date"> #if $cur_airs_next #set $ldatetime = $sbdatetime.sbdatetime.convert_to_setting($network_timezones.parse_date_time($cur_airs_next,$curShow.airs,$curShow.network)) - <span class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdate($ldatetime)</span> + <span class="${fuzzydate}"> + #try + $sbdatetime.sbdatetime.sbfdate($ldatetime) + #except ValueError + Invalid date + #end try + </span> #else #set $output_html = '?' #if None is not $display_status @@ -536,7 +542,13 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name)) #if $cur_airs_next #set $ldatetime = $sbdatetime.sbdatetime.convert_to_setting($network_timezones.parse_date_time($cur_airs_next,$curShow.airs,$curShow.network)) - <td align="center" class="nowrap"><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdate($ldatetime)</div><span class="sort_data">$calendar.timegm($ldatetime.timetuple())</span></td> + <td align="center" class="nowrap"><div class="${fuzzydate}"> + #try + $sbdatetime.sbdatetime.sbfdate($ldatetime) + #except ValueError + Invalid date + #end try + </div><span class="sort_data">$calendar.timegm($ldatetime.timetuple())</span></td> #else: <td align="center" class="nowrap"></td> #end if diff --git a/gui/slick/interfaces/default/inc_top.tmpl b/gui/slick/interfaces/default/inc_top.tmpl index e67363a9b98535416a3dac990465a42c8afc6731..07950ecedc8361cf458c944d8f56b24842035257 100644 --- a/gui/slick/interfaces/default/inc_top.tmpl +++ b/gui/slick/interfaces/default/inc_top.tmpl @@ -90,7 +90,7 @@ \$("#SubMenu a:contains('Clear History')").addClass('btn clearhistory').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear History'); \$("#SubMenu a:contains('Trim History')").addClass('btn trimhistory').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trim History'); \$("#SubMenu a[href$='/errorlogs/clearerrors/']").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear Errors'); - #if sickbeard.GIT_USERNAME and sickbeard.GIT_PASSWORD and sickbeard.GIT_AUTOISSUES == 1: + #if sickbeard.GIT_USERNAME and sickbeard.GIT_PASSWORD: \$("#SubMenu a[href$='/errorlogs/submit_errors/']").addClass('btn').html('<span class="ui-icon ui-icon-arrowreturnthick-1-n pull-left"></span> Submit Errors'); #end if \$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Re-scan'); @@ -253,7 +253,13 @@ </span> </div> #end if - + + #if $sickbeard.BRANCH and $sickbeard.BRANCH != 'master' and not $sickbeard.DEVELOPER + <div class="alert alert-danger upgrade-notification" role="alert"> + <span>You're using the $sickbeard.BRANCH branch. Please use 'master' unless specifically asked</span> + </div> + #end if + #if $sickbeard.NEWEST_VERSION_STRING: <div class="alert alert-success upgrade-notification" role="alert"> <span>$sickbeard.NEWEST_VERSION_STRING</span> diff --git a/gui/slick/interfaces/default/manage_backlogOverview.tmpl b/gui/slick/interfaces/default/manage_backlogOverview.tmpl index 223c7199715a260c853bd218a357ca2962559cf9..e83b418017e51ef3eb5ab2ba98e715f2f780309c 100644 --- a/gui/slick/interfaces/default/manage_backlogOverview.tmpl +++ b/gui/slick/interfaces/default/manage_backlogOverview.tmpl @@ -46,25 +46,22 @@ #end if #set $totalWanted = 0 #set $totalQual = 0 -#set $totalSnatched = 0 #for $curShow in $sickbeard.showList: #set $totalWanted = $totalWanted + $showCounts[$curShow.indexerid][$Overview.WANTED] #set $totalQual = $totalQual + $showCounts[$curShow.indexerid][$Overview.QUAL] -#set $totalSnatched = $totalSnatched + $showCounts[$curShow.indexerid][$Overview.SNATCHED] #end for <div class="h2footer pull-right"> <span class="listing-key wanted">Wanted: <b>$totalWanted</b></span> <span class="listing-key qual">Low Quality: <b>$totalQual</b></span> - <span class="listing-key snatched">Snatched: <b>$totalSnatched</b></span> </div><br/> <div class="float-left"> Jump to Show <select id="pickShow" class="form-control form-control-inline input-sm"> #for $curShow in sorted($sickbeard.showList, key = operator.attrgetter('name')): - #if $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED] + $showCounts[$curShow.indexerid][$Overview.SNATCHED] != 0: + #if $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED] != 0: <option value="$curShow.indexerid">$curShow.name</option> #end if #end for @@ -75,7 +72,7 @@ Jump to Show #for $curShow in sorted($sickbeard.showList, key = operator.attrgetter('name')): -#if $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED] + $showCounts[$curShow.indexerid][$Overview.SNATCHED] == 0: +#if $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED] == 0: #continue #end if @@ -85,7 +82,6 @@ Jump to Show <div class="pull-right"> <span class="listing-key wanted">Wanted: <b>$showCounts[$curShow.indexerid][$Overview.WANTED]</b></span> <span class="listing-key qual">Low Quality: <b>$showCounts[$curShow.indexerid][$Overview.QUAL]</b></span> - <span class="listing-key snatched">Snatched: <b>$showCounts[$curShow.indexerid][$Overview.SNATCHED]</b></span> <a class="btn btn-inline forceBacklog" href="$sbRoot/manage/backlogShow?indexer_id=$curShow.indexerid"><i class="icon-play-circle icon-white"></i> Force Backlog</a> </div> </td> @@ -101,7 +97,7 @@ Jump to Show #continue #end try - #if $overview not in ($Overview.QUAL, $Overview.WANTED, $Overview.SNATCHED): + #if $overview not in ($Overview.QUAL, $Overview.WANTED): #continue #end if diff --git a/gui/slick/js/configProviders.js b/gui/slick/js/configProviders.js index da21be5bc75f5df3e1745f2ae1064ac7e057d249..aa9965afd6c57f2f4ff32583e2e1c1f46b5a8a22 100644 --- a/gui/slick/js/configProviders.js +++ b/gui/slick/js/configProviders.js @@ -80,9 +80,9 @@ $(document).ready(function(){ } - $.fn.addTorrentRssProvider = function (id, name, url, cookies) { + $.fn.addTorrentRssProvider = function (id, name, url, cookies, titleTAG) { - var newData = [name, url, cookies]; + var newData = [name, url, cookies, titleTAG]; torrentRssProviders[id] = newData; $('#editATorrentRssProvider').addOption(id, name); @@ -122,9 +122,10 @@ $(document).ready(function(){ } - $.fn.updateTorrentRssProvider = function (id, url, cookies) { + $.fn.updateTorrentRssProvider = function (id, url, cookies, titleTAG) { torrentRssProviders[id][1] = url; torrentRssProviders[id][2] = cookies; + torrentRssProviders[id][3] = titleTAG; $(this).populateTorrentRssSection(); $(this).makeTorrentRssProviderString(); } @@ -277,7 +278,7 @@ $(document).ready(function(){ var selectedProvider = $('#editATorrentRssProvider :selected').val(); if (selectedProvider == 'addTorrentRss') { - var data = ['','','']; + var data = ['','','','title']; $('#torrentrss_add_div').show(); $('#torrentrss_update_div').hide(); } else { @@ -289,15 +290,18 @@ $(document).ready(function(){ $('#torrentrss_name').val(data[0]); $('#torrentrss_url').val(data[1]); $('#torrentrss_cookies').val(data[2]); + $('#torrentrss_titleTAG').val(data[3]); if (selectedProvider == 'addTorrentRss') { $('#torrentrss_name').removeAttr("disabled"); $('#torrentrss_url').removeAttr("disabled"); $('#torrentrss_cookies').removeAttr("disabled"); + $('#torrentrss_titleTAG').removeAttr("disabled"); } else { $('#torrentrss_name').attr("disabled", "disabled"); $('#torrentrss_url').removeAttr("disabled"); $('#torrentrss_cookies').removeAttr("disabled"); + $('#torrentrss_titleTAG').removeAttr("disabled"); $('#torrentrss_delete').removeAttr("disabled"); } @@ -386,7 +390,7 @@ $(document).ready(function(){ }); - $('#torrentrss_url,#torrentrss_cookies').change(function(){ + $('#torrentrss_url,#torrentrss_cookies,#torrentrss_titleTAG').change(function(){ var selectedProvider = $('#editATorrentRssProvider :selected').val(); @@ -395,8 +399,9 @@ $(document).ready(function(){ var url = $('#torrentrss_url').val(); var cookies = $('#torrentrss_cookies').val(); + var titleTAG = $('#torrentrss_titleTAG').val(); - $(this).updateTorrentRssProvider(selectedProvider, url, cookies); + $(this).updateTorrentRssProvider(selectedProvider, url, cookies, titleTAG); }); $('body').on('change', '#editAProvider',function(){ @@ -504,7 +509,8 @@ $(document).ready(function(){ var name = $('#torrentrss_name').val(); var url = $('#torrentrss_url').val(); var cookies = $('#torrentrss_cookies').val(); - var params = { name: name, url: url, cookies: cookies} + var titleTAG = $('#torrentrss_titleTAG').val(); + var params = { name: name, url: url, cookies: cookies, titleTAG: titleTAG} // send to the form with ajax, get a return value $.getJSON(sbRoot + '/config/providers/canAddTorrentRssProvider', params, @@ -514,7 +520,7 @@ $(document).ready(function(){ return; } - $(this).addTorrentRssProvider(data.success, name, url, cookies); + $(this).addTorrentRssProvider(data.success, name, url, cookies, titleTAG); $(this).refreshEditAProvider(); }); }); diff --git a/lib/shutil_custom/__init__.py b/lib/shutil_custom/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..32cdd9bf36c8b7f90c2a3d00b74d679112a6f74f --- /dev/null +++ b/lib/shutil_custom/__init__.py @@ -0,0 +1,52 @@ +import os +import platform +import stat +try: + from shutil import SpecialFileError, Error +except: + from shutil import Error +from shutil import _samefile + + +def copyfile_custom(src, dst): + """Copy data from src to dst""" + if _samefile(src, dst): + raise Error("`%s` and `%s` are the same file" % (src, dst)) + + for fn in [src, dst]: + try: + st = os.stat(fn) + except OSError: + # File most likely does not exist + pass + else: + # XXX What about other special files? (sockets, devices...) + if stat.S_ISFIFO(st.st_mode): + try: + raise SpecialFileError("`%s` is a named pipe" % fn) + except NameError: + raise Error("`%s` is a named pipe" % fn) + + try: + # Windows + O_BINARY = os.O_BINARY + except: + O_BINARY = 0 + + READ_FLAGS = os.O_RDONLY | O_BINARY + WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | O_BINARY + BUFFER_SIZE = 128*1024 + + try: + fin = os.open(src, READ_FLAGS) + fout = os.open(dst, WRITE_FLAGS) + for x in iter(lambda: os.read(fin, BUFFER_SIZE), ""): + os.write(fout, x) + except Exception as e: + raise e + finally: + try: + os.close(fin) + os.close(fout) + except: + pass diff --git a/setup.py b/setup.py index a3e2eb89c6228633cbb39e9c6bb6b9d70f7a6ec1..5a694247417c3f73bdaf2953a4d9e02365a400d1 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,6 @@ import urllib import ConfigParser import sys import os -import shutil import zipfile import subprocess import fnmatch @@ -11,6 +10,11 @@ import googlecode_upload from distutils.core import setup +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + try: import py2exe except: diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 9ca4047022bd7dbaed0924d9f5fd84610ca78320..0ed591483d544d01f585f3ae9052c471c82949d6 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -25,6 +25,9 @@ import os import re import os.path import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom from threading import Lock import sys @@ -35,7 +38,7 @@ from sickbeard import providers, metadata, config, webserveInit from sickbeard.providers.generic import GenericProvider from providers import ezrss, btn, newznab, womble, thepiratebay, oldpiratebay, torrentleech, kat, iptorrents, \ omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, hounddawgs, nextgen, speedcd, nyaatorrents, fanzub, torrentbytes, animezb, \ - freshontv, bitsoup, t411, tokyotoshokan, shazbat, rarbg, alpharatio, tntvillage + freshontv, bitsoup, t411, tokyotoshokan, shazbat, rarbg, alpharatio, tntvillage, binsearch from sickbeard.config import CheckSection, check_setting_int, check_setting_str, check_setting_float, ConfigMigrator, \ naming_ep_type from sickbeard import searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser, \ @@ -124,6 +127,7 @@ GIT_USERNAME = None GIT_PASSWORD = None GIT_PATH = None GIT_AUTOISSUES = False +DEVELOPER = False INIT_LOCK = Lock() started = False @@ -170,6 +174,7 @@ CACHE_DIR = None ACTUAL_CACHE_DIR = None ROOT_DIRS = None UPDATE_SHOWS_ON_START = False +UPDATE_SHOWS_ON_SNATCH = False TRASH_REMOVE_SHOW = False TRASH_ROTATE_LOGS = False SORT_ARTICLE = False @@ -261,6 +266,8 @@ NZBS_HASH = None WOMBLE = False +BINSEARCH = False + OMGWTFNZBS = False OMGWTFNZBS_USERNAME = None OMGWTFNZBS_APIKEY = None @@ -405,7 +412,7 @@ TRAKT_USERNAME = None TRAKT_PASSWORD = None TRAKT_REMOVE_WATCHLIST = False TRAKT_REMOVE_SERIESLIST = False -TRAKT_USE_WATCHLIST = False +TRAKT_SYNC_WATCHLIST = False TRAKT_METHOD_ADD = 0 TRAKT_START_PAUSED = False TRAKT_USE_RECOMMENDED = False @@ -516,10 +523,10 @@ def initialize(consoleLogging=True): TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_LABEL, TORRENT_LABEL_ANIME, TORRENT_VERIFY_CERT, TORRENT_RPCURL, TORRENT_AUTH_TYPE, \ USE_KODI, KODI_ALWAYS_ON, KODI_NOTIFY_ONSNATCH, KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, \ KODI_UPDATE_LIBRARY, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, BACKLOG_FREQUENCY, \ - USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, TRAKT_DISABLE_SSL_VERIFY, TRAKT_TIMEOUT, \ + USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_REMOVE_WATCHLIST, TRAKT_SYNC_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, TRAKT_DISABLE_SSL_VERIFY, TRAKT_TIMEOUT, \ USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \ PLEX_SERVER_HOST, PLEX_SERVER_TOKEN, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \ - showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, SORT_ARTICLE, showList, loadingShowList, \ + showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, UPDATE_SHOWS_ON_SNATCH, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, SORT_ARTICLE, showList, loadingShowList, \ NEWZNAB_DATA, NZBS, NZBS_UID, NZBS_HASH, INDEXER_DEFAULT, INDEXER_TIMEOUT, USENET_RETENTION, TORRENT_DIR, \ QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, DAILYSEARCH_STARTUP, \ GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD, USE_FREEMOBILE, FREEMOBILE_ID, FREEMOBILE_APIKEY, FREEMOBILE_NOTIFY_ONSNATCH, FREEMOBILE_NOTIFY_ONDOWNLOAD, FREEMOBILE_NOTIFY_ONSUBTITLEDOWNLOAD, \ @@ -533,7 +540,7 @@ def initialize(consoleLogging=True): showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TIMEZONE_DISPLAY, \ NAMING_PATTERN, NAMING_MULTI_EP, NAMING_ANIME_MULTI_EP, NAMING_FORCE_FOLDERS, NAMING_ABD_PATTERN, NAMING_CUSTOM_ABD, NAMING_SPORTS_PATTERN, NAMING_CUSTOM_SPORTS, NAMING_ANIME_PATTERN, NAMING_CUSTOM_ANIME, NAMING_STRIP_YEAR, \ RENAME_EPISODES, AIRDATE_EPISODES, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, \ - WOMBLE, OMGWTFNZBS, OMGWTFNZBS_USERNAME, OMGWTFNZBS_APIKEY, providerList, newznabProviderList, torrentRssProviderList, \ + WOMBLE, BINSEARCH, OMGWTFNZBS, OMGWTFNZBS_USERNAME, OMGWTFNZBS_APIKEY, providerList, newznabProviderList, torrentRssProviderList, \ EXTRA_SCRIPTS, USE_TWITTER, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, DAILYSEARCH_FREQUENCY, \ USE_BOXCAR, BOXCAR_USERNAME, BOXCAR_PASSWORD, BOXCAR_NOTIFY_ONDOWNLOAD, BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD, BOXCAR_NOTIFY_ONSNATCH, \ USE_BOXCAR2, BOXCAR2_ACCESSTOKEN, BOXCAR2_NOTIFY_ONDOWNLOAD, BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD, BOXCAR2_NOTIFY_ONSNATCH, \ @@ -551,7 +558,7 @@ def initialize(consoleLogging=True): AUTOPOSTPROCESSER_FREQUENCY, SHOWUPDATE_HOUR, DEFAULT_AUTOPOSTPROCESSER_FREQUENCY, MIN_AUTOPOSTPROCESSER_FREQUENCY, \ ANIME_DEFAULT, NAMING_ANIME, ANIMESUPPORT, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \ ANIME_SPLIT_HOME, SCENE_DEFAULT, PLAY_VIDEOS, DOWNLOAD_URL, BACKLOG_DAYS, GIT_ORG, GIT_REPO, GIT_USERNAME, GIT_PASSWORD, \ - GIT_AUTOISSUES, gh + GIT_AUTOISSUES, DEVELOPER, gh if __INITIALIZED__: return False @@ -583,6 +590,7 @@ def initialize(consoleLogging=True): # git login info GIT_USERNAME = check_setting_str(CFG, 'General', 'git_username', '') GIT_PASSWORD = check_setting_str(CFG, 'General', 'git_password', '', censor_log=True) + DEVELOPER = bool(check_setting_int(CFG, 'General', 'developer', 0)) # debugging DEBUG = bool(check_setting_int(CFG, 'General', 'debug', 0)) @@ -718,6 +726,7 @@ def initialize(consoleLogging=True): ANON_REDIRECT = '' UPDATE_SHOWS_ON_START = bool(check_setting_int(CFG, 'General', 'update_shows_on_start', 0)) + UPDATE_SHOWS_ON_SNATCH = bool(check_setting_int(CFG, 'General', 'update_shows_on_snatch', 0)) TRASH_REMOVE_SHOW = bool(check_setting_int(CFG, 'General', 'trash_remove_show', 0)) TRASH_ROTATE_LOGS = bool(check_setting_int(CFG, 'General', 'trash_rotate_logs', 0)) @@ -972,7 +981,7 @@ def initialize(consoleLogging=True): TRAKT_PASSWORD = check_setting_str(CFG, 'Trakt', 'trakt_password', '', censor_log=True) TRAKT_REMOVE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_watchlist', 0)) TRAKT_REMOVE_SERIESLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_serieslist', 0)) - TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0)) + TRAKT_SYNC_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_sync_watchlist', 0)) TRAKT_METHOD_ADD = check_setting_int(CFG, 'Trakt', 'trakt_method_add', 0) TRAKT_START_PAUSED = bool(check_setting_int(CFG, 'Trakt', 'trakt_start_paused', 0)) TRAKT_USE_RECOMMENDED = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_recommended', 0)) @@ -1502,7 +1511,7 @@ def save_config(): new_config['General'] = {} new_config['General']['git_autoissues'] = int(GIT_AUTOISSUES) new_config['General']['git_username'] = GIT_USERNAME - new_config['General']['git_password'] = GIT_PASSWORD + new_config['General']['git_password'] = helpers.encrypt(GIT_PASSWORD, ENCRYPTION_VERSION) new_config['General']['git_reset'] = int(GIT_RESET) new_config['General']['branch'] = BRANCH new_config['General']['git_remote'] = GIT_REMOTE @@ -1575,6 +1584,7 @@ def save_config(): new_config['General']['naming_anime'] = int(NAMING_ANIME) new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) new_config['General']['update_shows_on_start'] = int(UPDATE_SHOWS_ON_START) + new_config['General']['update_shows_on_snatch'] = int(UPDATE_SHOWS_ON_SNATCH) new_config['General']['trash_remove_show'] = int(TRASH_REMOVE_SHOW) new_config['General']['trash_rotate_logs'] = int(TRASH_ROTATE_LOGS) new_config['General']['sort_article'] = int(SORT_ARTICLE) @@ -1614,6 +1624,7 @@ def save_config(): new_config['General']['ignore_words'] = IGNORE_WORDS new_config['General']['require_words'] = REQUIRE_WORDS new_config['General']['calendar_unprotected'] = int(CALENDAR_UNPROTECTED) + new_config['General']['developer'] = int(DEVELOPER) new_config['Blackhole'] = {} new_config['Blackhole']['nzb_dir'] = NZB_DIR @@ -1865,7 +1876,7 @@ def save_config(): new_config['Trakt']['trakt_password'] = helpers.encrypt(TRAKT_PASSWORD, ENCRYPTION_VERSION) new_config['Trakt']['trakt_remove_watchlist'] = int(TRAKT_REMOVE_WATCHLIST) new_config['Trakt']['trakt_remove_serieslist'] = int(TRAKT_REMOVE_SERIESLIST) - new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST) + new_config['Trakt']['trakt_sync_watchlist'] = int(TRAKT_SYNC_WATCHLIST) new_config['Trakt']['trakt_method_add'] = int(TRAKT_METHOD_ADD) new_config['Trakt']['trakt_start_paused'] = int(TRAKT_START_PAUSED) new_config['Trakt']['trakt_use_recommended'] = int(TRAKT_USE_RECOMMENDED) diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 7d0503e75420750d6b9aabd5b21fbe7502d8e74f..29895e95606f32695110c1b78c0a7dd4594f1d22 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -22,7 +22,6 @@ import os import ctypes import random import re -import shutil import socket import stat import tempfile @@ -58,6 +57,11 @@ from sickbeard import clients from cachecontrol import CacheControl, caches from itertools import izip, cycle +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + urllib._urlopener = classes.SickBeardURLopener() @@ -349,12 +353,8 @@ def listMediaFiles(path): return files -def copyFile(srcFile, destFile): - if isPosix(): - subprocess.call(['cp', srcFile, destFile]) - else: - ek.ek(shutil.copyfile, srcFile, destFile) - +def copyFile(srcFile, destFile): + ek.ek(shutil.copyfile, srcFile, destFile) try: ek.ek(shutil.copymode, srcFile, destFile) except OSError: @@ -378,12 +378,6 @@ def link(src, dst): else: os.link(src, dst) -def isPosix(): - if os.name.startswith('posix'): - return True - else: - return False - def hardlinkFile(srcFile, destFile): try: @@ -923,7 +917,7 @@ def _check_against_names(nameInQuestion, show, season=-1): return False -def get_show(name, tryIndexers=False): +def get_show(name, tryIndexers=False, trySceneExceptions=False): if not sickbeard.showList: return @@ -936,11 +930,18 @@ def get_show(name, tryIndexers=False): if cache: fromCache = True showObj = findCertainShow(sickbeard.showList, int(cache)) - + + #try indexers if not showObj and tryIndexers: showObj = findCertainShow(sickbeard.showList, searchIndexerForShowID(full_sanitizeSceneName(name), ui=classes.ShowListUI)[2]) - + + #try scene exceptions + if not showObj and trySceneExceptions: + ShowID = sickbeard.scene_exceptions.get_scene_exception_by_name(name)[0] + if ShowID: + showObj = findCertainShow(sickbeard.showList, int(ShowID)) + # add show to cache if showObj and not fromCache: sickbeard.name_cache.addNameToCache(name, showObj.indexerid) diff --git a/sickbeard/metadata/mede8er.py b/sickbeard/metadata/mede8er.py index 64db23f7c98aad2410fb806d1d117468290272f7..f4bd9d618d2fec1613f01e58b0e528d9b0fef573 100644 --- a/sickbeard/metadata/mede8er.py +++ b/sickbeard/metadata/mede8er.py @@ -131,7 +131,7 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): # check for title and id try: - if myShow['seriesname'] == None or myShow['seriesname'] == "" or myShow['id'] == None or myShow['id'] == "": + if getattr(myShow, 'seriesname', None) == None or getattr(myShow, 'seriesname', "") == "" or getattr(myShow, 'id', None) == None or getattr(myShow, 'id', "") == "": logger.log(u"Incomplete info for show with id " + str(show_obj.indexerid) + " on tvdb, skipping it", logger.ERROR) return False except sickbeard.indexer_attributenotfound: @@ -139,24 +139,21 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): return False SeriesName = etree.SubElement(tv_node, "title") - if myShow['seriesname'] != None: - SeriesName.text = myShow['seriesname'] - else: - SeriesName.text = "" - + SeriesName.text = myShow['seriesname'] + Genres = etree.SubElement(tv_node, "genres") - if myShow["genre"] != None: + if getattr(myShow, "genre", None) != None: for genre in myShow['genre'].split('|'): if genre and genre.strip(): cur_genre = etree.SubElement(Genres, "Genre") cur_genre.text = genre.strip() FirstAired = etree.SubElement(tv_node, "premiered") - if myShow['firstaired'] != None: + if getattr(myShow, 'firstaired', None) != None: FirstAired.text = myShow['firstaired'] year = etree.SubElement(tv_node, "year") - if myShow["firstaired"] != None: + if getattr(myShow, "firstaired", None) != None: try: year_text = str(datetime.datetime.strptime(myShow["firstaired"], '%Y-%m-%d').year) if year_text: @@ -167,7 +164,7 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): if getattr(myShow, 'overview', None) is not None: plot.text = myShow["overview"] - if myShow['rating'] != None: + if getattr(myShow, 'rating', None) != None: try: rating = int((float(myShow['rating']) * 10)) except ValueError: @@ -178,24 +175,24 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): Rating.text = rating_text Status = etree.SubElement(tv_node, "status") - if myShow['status'] != None: + if getattr(myShow, 'status', None) != None: Status.text = myShow['status'] mpaa = etree.SubElement(tv_node, "mpaa") - if myShow["contentrating"] != None: + if getattr(myShow, "contentrating", None) != None: mpaa.text = myShow["contentrating"] IMDB_ID = etree.SubElement(tv_node, "id") - if myShow['imdb_id'] != None: + if getattr(myShow, 'imdb_id', None) != None: IMDB_ID.attrib["moviedb"] = "imdb" IMDB_ID.text = myShow['imdb_id'] indexerid = etree.SubElement(tv_node, "indexerid") - if myShow['id'] != None: + if getattr(myShow, 'id', None) != None: indexerid.text = myShow['id'] Runtime = etree.SubElement(tv_node, "runtime") - if myShow['runtime'] != None: + if getattr(myShow, 'runtime', None) != None: Runtime.text = myShow['runtime'] cast = etree.SubElement(tv_node, "cast") @@ -264,10 +261,10 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): # root (or single) episode # default to today's date for specials if firstaired is not set - if myEp['firstaired'] == None and ep_obj.season == 0: + if getattr(myEp, 'firstaired', None) == None and ep_obj.season == 0: myEp['firstaired'] = str(datetime.date.fromordinal(1)) - if myEp['episodename'] == None or myEp['firstaired'] == None: + if getattr(myEp, 'episodename', None) == None or getattr(myEp, 'firstaired', None) == None: return None episode = movie @@ -285,7 +282,7 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): EpisodeNumber.text = str(ep_obj.episode) year = etree.SubElement(episode, "year") - if myShow["firstaired"] != None: + if getattr(myShow, "firstaired", None) != None: try: year_text = str(datetime.datetime.strptime(myShow["firstaired"], '%Y-%m-%d').year) if year_text: @@ -294,7 +291,7 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): pass plot = etree.SubElement(episode, "plot") - if myShow["overview"] != None: + if getattr(myShow, "overview", None) != None: plot.text = myShow["overview"] Overview = etree.SubElement(episode, "episodeplot") @@ -319,12 +316,12 @@ class Mede8erMetadata(mediabrowser.MediaBrowserMetadata): Rating.text = rating_text director = etree.SubElement(episode, "director") - director_text = myEp['director'] + director_text = getattr(myEp, 'director', None) if director_text != None: director.text = director_text credits = etree.SubElement(episode, "credits") - credits_text = myEp['writer'] + credits_text = getattr(myEp, 'writer', None) if credits_text != None: credits.text = credits_text diff --git a/sickbeard/name_parser/parser.py b/sickbeard/name_parser/parser.py index e981269af8233548495d6b35c93e380c461ac99c..b68756c55d4b8860fdeeb04bad6e0f9af6c2028b 100644 --- a/sickbeard/name_parser/parser.py +++ b/sickbeard/name_parser/parser.py @@ -35,12 +35,13 @@ class NameParser(object): NORMAL_REGEX = 1 ANIME_REGEX = 2 - def __init__(self, file_name=True, showObj=None, tryIndexers=False, convert=False, + def __init__(self, file_name=True, showObj=None, tryIndexers=False, trySceneExceptions=False, convert=False, naming_pattern=False): self.file_name = file_name self.showObj = showObj self.tryIndexers = tryIndexers + self.trySceneExceptions = trySceneExceptions self.convert = convert self.naming_pattern = naming_pattern @@ -191,7 +192,7 @@ class NameParser(object): show = None if not self.naming_pattern: # try and create a show object for this result - show = helpers.get_show(bestResult.series_name, self.tryIndexers) + show = helpers.get_show(bestResult.series_name, self.tryIndexers, self.trySceneExceptions) # confirm passed in show object indexer id matches result show object indexer id if show: diff --git a/sickbeard/notifiers/pushbullet.py b/sickbeard/notifiers/pushbullet.py index 64c24fedf62c84c43b8943007d624a031d18de01..bb3a1f4e06edc3aecd255fd8b1ef030b67ba186b 100644 --- a/sickbeard/notifiers/pushbullet.py +++ b/sickbeard/notifiers/pushbullet.py @@ -53,7 +53,7 @@ class PushbulletNotifier: if sickbeard.USE_PUSHBULLET: update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendPushbullet(pushbullet_api=None, event=title, message=update_text + new_version, method="POST") + self._sendPushbullet(pushbullet_api=None, event=title, message=update_text + new_version, notificationType="note", method="POST") def _sendPushbullet(self, pushbullet_api=None, pushbullet_device=None, event=None, message=None, notificationType=None, method=None, force=False): diff --git a/sickbeard/notifiers/trakt.py b/sickbeard/notifiers/trakt.py index 2796f1f81122fecab5985367f1d1a0b99a902b0c..abc845daf4f0d285c26554a88d2eced2d151ce86 100644 --- a/sickbeard/notifiers/trakt.py +++ b/sickbeard/notifiers/trakt.py @@ -80,10 +80,6 @@ class TraktNotifier: # update library trakt_api.traktRequest("sync/collection", data, method='POST') - # remove from watchlist - if sickbeard.TRAKT_REMOVE_WATCHLIST: - trakt_api.traktRequest("sync/watchlist/remove", data, method='POST') - if sickbeard.TRAKT_REMOVE_SERIESLIST: data = { 'shows': [ @@ -105,6 +101,124 @@ class TraktNotifier: except (traktException, traktAuthException, traktServerBusy) as e: logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) + def update_watchlist (self, show_obj = None, s = None, e = None, data_show = None, data_episode = None, update = "add"): + + """ + Sends a request to trakt indicating that the given episode is part of our library. + + show_obj: The TVShow object to add to trakt + s: season number + e: episode number + data_show: structured object of shows traktv type + data_episode: structured object of episodes traktv type + update: type o action add or remove + """ + + trakt_api = TraktAPI(sickbeard.TRAKT_API_KEY, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD) + + if sickbeard.USE_TRAKT: + + data = {} + try: + # URL parameters + if show_obj is not None: + trakt_id = sickbeard.indexerApi(show_obj.indexer).config['trakt_id'] + data = { + 'shows': [ + { + 'title': show_obj.name, + 'year': show_obj.startyear, + 'ids': {}, + } + ] + } + + if trakt_id == 'tvdb_id': + data['shows'][0]['ids']['tvdb'] = show_obj.indexerid + else: + data['shows'][0]['ids']['tvrage'] = show_obj.indexerid + elif data_show is not None: + data.update(data_show) + else: + logger.log(u"there's a coding problem contact developer. It's needed to be provided at lest one of the two: data_show or show_obj", logger.WARNING) + return False + + if data_episode is not None: + data['shows'][0].update(data_episode) + + elif s is not None: + # traktv URL parameters + season = { + 'season': [ + { + 'number': s, + } + ] + } + + if e is not None: + # traktv URL parameters + episode = { + 'episodes': [ + { + 'number': e + } + ] + } + + season['season'][0].update(episode) + + data['shows'][0].update(season) + + trakt_url = "sync/watchlist" + if update=="remove": + trakt_url += "/remove" + + trakt_api.traktRequest(trakt_url, data, method='POST') + + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) + return False + + return True + + def trakt_show_data_generate(self, data): + + showList = [] + for indexer, indexerid, title, year in data: + trakt_id = sickbeard.indexerApi(indexer).config['trakt_id'] + show = {'title': title, 'year': year, 'ids': {}} + if trakt_id == 'tvdb_id': + show['ids']['tvdb'] = indexerid + else: + show['ids']['tvrage'] = indexerid + showList.append(show) + + post_data = {'shows': showList} + + return post_data + + def trakt_episode_data_generate(self, data): + + # Find how many unique season we have + uniqueSeasons = [] + for season, episode in data: + if season not in uniqueSeasons: + uniqueSeasons.append(season) + + #build the query + seasonsList = [] + for searchedSeason in uniqueSeasons: + episodesList = [] + for season, episode in data: + if season == searchedSeason: + episodesList.append({'number': episode}) + seasonsList.append({'number': searchedSeason, 'episodes': episodesList}) + + post_data = {'seasons': seasonsList} + + return post_data + def test_notify(self, username, password, disable_ssl): """ Sends a test notification to trakt with the given authentication info and returns a boolean diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index f19f55043634ca9b089fad5a0ddd3eae54ccbe7c..056dc4c27c7194ce9469c1a767fb3304a770b38b 100644 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -167,7 +167,10 @@ class PostProcessor(object): file_path_list = [] - base_name = ek.ek(os.path.basename, file_path).rpartition('.')[0] + if subfolders: + base_name = ek.ek(os.path.basename, file_path).rpartition('.')[0] + else: + base_name = file_path.rpartition('.')[0] if not base_name_only: base_name = base_name + '.' @@ -214,7 +217,7 @@ class PostProcessor(object): # figure out which files we want to delete file_list = [file_path] if associated_files: - file_list = file_list + self.list_associated_files(file_path) + file_list = file_list + self.list_associated_files(file_path, base_name_only=True, subfolders=True) if not file_list: self._log(u"There were no files associated with " + file_path + ", not deleting anything", logger.DEBUG) @@ -489,7 +492,7 @@ class PostProcessor(object): name = helpers.remove_non_release_groups(helpers.remove_extension(name)) # parse the name to break it into show name, season, and episode - np = NameParser(file, tryIndexers=True, convert=True) + np = NameParser(file, tryIndexers=True, trySceneExceptions=True, convert=True) parse_result = np.parse(name) # show object @@ -897,6 +900,7 @@ class PostProcessor(object): # update the ep info before we rename so the quality & release name go into the name properly sql_l = [] + trakt_data = [] for cur_ep in [ep_obj] + ep_obj.relatedEps: with cur_ep.lock: @@ -928,6 +932,15 @@ class PostProcessor(object): sql_l.append(cur_ep.get_sql()) + trakt_data.append((cur_ep.season, cur_ep.episode)) + + data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) + + if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC_WATCHLIST and sickbeard.TRAKT_REMOVE_WATCHLIST: + logger.log(u"Remove episodes, showid: indexerid " + str(show.indexerid) + ", Title " + str(show.name) + " to Traktv Watchlist", logger.DEBUG) + if data: + notifiers.trakt_notifier.update_watchlist(show, data_episode=data, update="remove") + if len(sql_l) > 0: myDB = db.DBConnection() myDB.mass_action(sql_l) diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py index 7da8dc368475f0b550baaddfdfd00941de6a9390..4243b5cb8821dc17b66b18f9310605ac1523435c 100644 --- a/sickbeard/processTV.py +++ b/sickbeard/processTV.py @@ -19,7 +19,6 @@ from __future__ import with_statement import os -import shutil import stat import sickbeard @@ -36,6 +35,12 @@ from sickbeard import failedProcessor from lib.unrar2 import RarFile, RarInfo from lib.unrar2.rar_exceptions import * +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + + class ProcessResult: def __init__(self): self.result = True @@ -174,18 +179,18 @@ def processDir(dirName, nzbName=None, process_method=None, force=False, is_prior #Don't Link media when the media is extracted from a rar in the same path if process_method in ('hardlink', 'symlink') and videoInRar: - result.result = process_media(path, videoInRar, nzbName, 'move', force, is_priority, result) + process_media(path, videoInRar, nzbName, 'move', force, is_priority, result) delete_files(path, rarContent, result) for video in set(videoFiles) - set(videoInRar): - result.result = process_media(path, [video], nzbName, process_method, force, is_priority, result) + process_media(path, [video], nzbName, process_method, force, is_priority, result) elif sickbeard.DELRARCONTENTS and videoInRar: - result.result = process_media(path, videoInRar, nzbName, process_method, force, is_priority, result) + process_media(path, videoInRar, nzbName, process_method, force, is_priority, result) delete_files(path, rarContent, result, True) for video in set(videoFiles) - set(videoInRar): - result.result = process_media(path, [video], nzbName, process_method, force, is_priority, result) + process_media(path, [video], nzbName, process_method, force, is_priority, result) else: for video in videoFiles: - result.result = process_media(path, [video], nzbName, process_method, force, is_priority, result) + process_media(path, [video], nzbName, process_method, force, is_priority, result) #Process Video File in all TV Subdir for dir in [x for x in dirs if validateDir(path, x, nzbNameOriginal, failed, result)]: @@ -367,7 +372,7 @@ def unRAR(path, rarFiles, force, result): result.result = False continue except NoFileToExtract: - result.output += logHelper(u"Failed Unrar archive (0): Unrar: No file to extract, file already exist?".format(archive), logger.ERROR) + result.output += logHelper(u"Failed Unrar archive {0}: Unrar: No file to extract, file already exist?".format(archive), logger.ERROR) result.result = False continue except GenericRARError: @@ -414,7 +419,7 @@ def already_postprocessed(dirName, videofile, force, result): #Needed if we have downloaded the same episode @ different quality #But we need to make sure we check the history of the episode we're going to PP, and not others - np = NameParser(dirName, tryIndexers=True, convert=True) + np = NameParser(dirName, tryIndexers=True, trySceneExceptions=True, convert=True) try: #if it fails to find any info (because we're doing an unparsable folder (like the TV root dir) it will throw an exception, which we want to ignore parse_result = np.parse(dirName) except: #ignore the exception, because we kind of expected it, but create parse_result anyway so we can perform a check on it. diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py index d8af81879bdbc31a3594973c07e0367773d34abe..17b8cd2e0c06140e1997d1ad9aa73c80d07de56a 100755 --- a/sickbeard/providers/__init__.py +++ b/sickbeard/providers/__init__.py @@ -44,6 +44,7 @@ __all__ = ['ezrss', 'shazbat', 'rarbg', 'tntvillage', + 'binsearch', ] import sickbeard @@ -166,6 +167,7 @@ def makeTorrentRssProvider(configString): return None cookies = None + titleTAG = 'title' search_mode = 'eponly' search_fallback = 0 enable_daily = 0 @@ -173,12 +175,14 @@ def makeTorrentRssProvider(configString): try: values = configString.split('|') - if len(values) == 8: + if len(values) == 9: + name, url, cookies, titleTAG, enabled, search_mode, search_fallback, enable_daily, enable_backlog = values + elif len(values) == 8: name, url, cookies, enabled, search_mode, search_fallback, enable_daily, enable_backlog = values else: name = values[0] url = values[1] - enabled = values[3] + enabled = values[4] except ValueError: logger.log(u"Skipping RSS Torrent provider string: '" + configString + "', incorrect format", logger.ERROR) @@ -189,7 +193,7 @@ def makeTorrentRssProvider(configString): except: return - newProvider = torrentRss.TorrentRssProvider(name, url, cookies, search_mode, search_fallback, enable_daily, + newProvider = torrentRss.TorrentRssProvider(name, url, cookies, titleTAG, search_mode, search_fallback, enable_daily, enable_backlog) newProvider.enabled = enabled == '1' diff --git a/sickbeard/providers/alpharatio.py b/sickbeard/providers/alpharatio.py index be7fd45799df4fc73b6fe61145080a8cb24d793a..9493ef9c58e0c0292e05b95a6b8b8ee4489a599a 100755 --- a/sickbeard/providers/alpharatio.py +++ b/sickbeard/providers/alpharatio.py @@ -189,9 +189,9 @@ class AlphaRatioProvider(generic.TorrentProvider): try: title = link.contents[0] download_url = self.urls['download'] % (url['href']) - id = link['href'].replace('torrents.php?id=', '').split('&')[0] - seeders = cells[len(cells)-2].string - leechers = cells[len(cells)-1].string + id = link['href'][-6:] + seeders = cells[len(cells)-2].contents[0] + leechers = cells[len(cells)-1].contents[0] except (AttributeError, TypeError): continue diff --git a/sickbeard/providers/binsearch.py b/sickbeard/providers/binsearch.py new file mode 100644 index 0000000000000000000000000000000000000000..a0ae8995fca115469a4fa624b8168d553ca4bf2b --- /dev/null +++ b/sickbeard/providers/binsearch.py @@ -0,0 +1,119 @@ +# Author: moparisthebest <admin@moparisthebest.com> +# +# 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/>. +import urllib +import re +import time + +import sickbeard + +import generic + +from sickbeard import logger +from sickbeard import tvcache +from sickbeard.exceptions import AuthException + +class BinSearchProvider(generic.NZBProvider): + def __init__(self): + generic.NZBProvider.__init__(self, "BinSearch") + self.enabled = False + self.cache = BinSearchCache(self) + self.urls = {'base_url': 'https://www.binsearch.info/'} + self.url = self.urls['base_url'] + + def isEnabled(self): + return self.enabled + +class BinSearchCache(tvcache.TVCache): + def __init__(self, provider): + tvcache.TVCache.__init__(self, provider) + # only poll Binsearch every 30 minutes max + self.minTime = 30 + + # compile and save our regular expressions + + # this pulls the title from the URL in the description + self.descTitleStart = re.compile('^.*https?://www\.binsearch\.info/.b=') + self.descTitleEnd = re.compile('&.*$') + + # these clean up the horrible mess of a title if the above fail + self.titleCleaners = [ + re.compile('.?yEnc.?\(\d+/\d+\)$'), + re.compile(' \[\d+/\d+\] '), + ] + + def _get_title_and_url(self, item): + """ + Retrieves the title and URL data from the item XML node + + item: An elementtree.ElementTree element representing the <item> tag of the RSS feed + + Returns: A tuple containing two strings representing title and URL respectively + """ + + title = item.get('description') + if title: + title = u'' + title + if self.descTitleStart.match(title): + title = self.descTitleStart.sub('', title) + title = self.descTitleEnd.sub('', title) + title = title.replace('+', '.') + else: + # just use the entire title, looks hard/impossible to parse + title = item.get('title') + if title: + for titleCleaner in self.titleCleaners: + title = titleCleaner.sub('', title) + + url = item.get('link') + if url: + url = url.replace('&', '&') + + return (title, url) + + def updateCache(self): + # check if we should update + if not self.shouldUpdate(): + return + + # clear cache + self._clearCache() + + # set updated + self.setLastUpdate() + + cl = [] + for group in ['alt.binaries.boneless','alt.binaries.misc','alt.binaries.hdtv','alt.binaries.hdtv.x264','alt.binaries.tv','alt.binaries.tvseries']: + url = self.provider.url + 'rss.php?' + urlArgs = {'max': 1000,'g': group} + + url += urllib.urlencode(urlArgs) + + logger.log(u"BinSearch cache update URL: " + url, logger.DEBUG) + + for item in self.getRSSFeed(url)['entries'] or []: + ci = self._parseItem(item) + if ci is not None: + cl.append(ci) + + if len(cl) > 0: + myDB = self._getDB() + myDB.mass_action(cl) + + def _checkAuth(self, data): + return data if data['feed'] and data['feed']['title'] != 'Invalid Link' else None + +provider = BinSearchProvider() diff --git a/sickbeard/providers/rsstorrent.py b/sickbeard/providers/rsstorrent.py index d6b4b15a18ad0b4780acc127860954e94227ae0e..6df14be4f308c24f143cdc8168859da08ee8531b 100644 --- a/sickbeard/providers/rsstorrent.py +++ b/sickbeard/providers/rsstorrent.py @@ -33,7 +33,7 @@ from lib.bencode import bdecode class TorrentRssProvider(generic.TorrentProvider): - def __init__(self, name, url, cookies='', search_mode='eponly', search_fallback=False, enable_daily=False, + def __init__(self, name, url, cookies='', titleTAG='title', search_mode='eponly', search_fallback=False, enable_daily=False, enable_backlog=False): generic.TorrentProvider.__init__(self, name) self.cache = TorrentRssCache(self) @@ -51,11 +51,13 @@ class TorrentRssProvider(generic.TorrentProvider): self.enable_daily = enable_daily self.enable_backlog = enable_backlog self.cookies = cookies + self.titleTAG = titleTAG def configStr(self): - return "%s|%s|%s|%d|%s|%d|%d|%d" % (self.name or '', + return "%s|%s|%s|%s|%d|%s|%d|%d|%d" % (self.name or '', self.url or '', self.cookies or '', + self.titleTAG or '', self.enabled, self.search_mode or '', self.search_fallback, @@ -74,7 +76,7 @@ class TorrentRssProvider(generic.TorrentProvider): def _get_title_and_url(self, item): - title = item.get('title') + title = item.get(self.titleTAG) if title: title = u'' + title title = title.replace(' ', '.') @@ -165,4 +167,4 @@ class TorrentRssCache(tvcache.TVCache): if self.provider.cookies: self.provider.headers.update({'Cookie': self.provider.cookies}) - return self.getRSSFeed(self.provider.url) \ No newline at end of file + return self.getRSSFeed(self.provider.url) diff --git a/sickbeard/search.py b/sickbeard/search.py index c649826290af4f2c9d511b55c2be2e428ce13efe..aa6995b0673d0fa9e8e1f9d1c1cdc19e9b782d7d 100644 --- a/sickbeard/search.py +++ b/sickbeard/search.py @@ -149,6 +149,7 @@ def snatchEpisode(result, endStatus=SNATCHED): # don't notify when we re-download an episode sql_l = [] + trakt_data = [] for curEpObj in result.episodes: with curEpObj.lock: if isFirstBestMatch(result): @@ -161,10 +162,22 @@ def snatchEpisode(result, endStatus=SNATCHED): if curEpObj.status not in Quality.DOWNLOADED: notifiers.notify_snatch(curEpObj._format_pattern('%SN - %Sx%0E - %EN - %QN') + " from " + result.provider.name) + trakt_data.append((curEpObj.season, curEpObj.episode)) + + data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) + + if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC_WATCHLIST: + logger.log(u"Add episodes, showid: indexerid " + str(result.show.indexerid) + ", Title " + str(result.show.name) + " to Traktv Watchlist", logger.DEBUG) + if data: + notifiers.trakt_notifier.update_watchlist(result.show, data_episode=data, update="add") + if len(sql_l) > 0: myDB = db.DBConnection() myDB.mass_action(sql_l) + if sickbeard.UPDATE_SHOWS_ON_SNATCH and not sickbeard.showQueueScheduler.action.isBeingUpdated(result.show) and result.show.status == "Continuing": + sickbeard.showQueueScheduler.action.updateShow(result.show, True) + return True diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py index 3b36c9ae7b76d80e6123313b22589ddf94dd23af..8a26aee4a371408dc0b694693490824194c69e69 100644 --- a/sickbeard/show_queue.py +++ b/sickbeard/show_queue.py @@ -25,7 +25,7 @@ import sickbeard from lib.imdb import _exceptions as imdb_exceptions from sickbeard.common import SKIPPED, WANTED from sickbeard.tv import TVShow -from sickbeard import exceptions, logger, ui, db +from sickbeard import exceptions, logger, ui, db, notifiers from sickbeard import generic_queue from sickbeard import name_cache from sickbeard.exceptions import ex @@ -386,6 +386,10 @@ class QueueItemAdd(ShowQueueItem): if sickbeard.TRAKT_SYNC: sickbeard.traktCheckerScheduler.action.addShowToTraktLibrary(self.show) + if sickbeard.TRAKT_SYNC_WATCHLIST: + logger.log(u"update watchlist") + notifiers.trakt_notifier.update_watchlist(self.show) + # Load XEM data to DB for show sickbeard.scene_numbering.xem_refresh(self.show.indexerid, self.show.indexer, force=True) diff --git a/sickbeard/traktChecker.py b/sickbeard/traktChecker.py index dfe5b5ac590d2abe2d361968a5f28b9820a45c88..59761e5449df2b1e127f41e5b6b13992112e61b0 100644 --- a/sickbeard/traktChecker.py +++ b/sickbeard/traktChecker.py @@ -27,34 +27,45 @@ from sickbeard.exceptions import ex from sickbeard import logger from sickbeard import helpers from sickbeard import search_queue -from sickbeard.common import SKIPPED, WANTED - +from sickbeard import db +from sickbeard import notifiers +from sickbeard.common import SNATCHED, SNATCHED_PROPER, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED, UNKNOWN, FAILED +from common import Quality, qualityPresetStrings, statusStrings from lib.trakt import * from trakt.exceptions import traktException, traktAuthException, traktServerBusy - class TraktChecker(): def __init__(self): self.todoWanted = [] self.trakt_api = TraktAPI(sickbeard.TRAKT_API_KEY, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD, sickbeard.TRAKT_DISABLE_SSL_VERIFY, sickbeard.TRAKT_TIMEOUT) + self.todoBacklog = [] + self.ShowWatchlist = [] + self.EpisodeWatchlist = [] def run(self, force=False): - try: - # add shows from trakt.tv watchlist - if sickbeard.TRAKT_USE_WATCHLIST: - self.todoWanted = [] # its about to all get re-added - if len(sickbeard.ROOT_DIRS.split('|')) < 2: - logger.log(u"No default root directory", logger.ERROR) - return - self.updateShows() - self.updateEpisodes() + if not sickbeard.USE_TRAKT: + logger.log(u"Trakt integrazione disabled, quit", logger.DEBUG) + return + + # add shows from trakt.tv watchlist + if sickbeard.TRAKT_SYNC_WATCHLIST: + self.todoWanted = [] # its about to all get re-added + if len(sickbeard.ROOT_DIRS.split('|')) < 2: + logger.log(u"No default root directory", logger.ERROR) + return + + try: + self.syncWatchlist() + except Exception: + logger.log(traceback.format_exc(), logger.DEBUG) - # sync trakt.tv library with sickrage library - if sickbeard.TRAKT_SYNC: - self.syncLibrary() - except Exception as e: - logger.log('Trakt: Error Syncing library. Reason: {0}'.format(str(e)), logger.DEBUG) + try: + # sync trakt.tv library with sickrage library + if sickbeard.TRAKT_SYNC: + self.syncLibrary() + except Exception: + logger.log(traceback.format_exc(), logger.DEBUG) def findShow(self, indexer, indexerid): traktShow = None @@ -143,20 +154,104 @@ class TraktChecker(): logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) return + def syncWatchlist(self): + + logger.log(u"Syncing Trakt.tv show watchlist", logger.DEBUG) + + logger.log(u"Getting ShowWatchlist", logger.DEBUG) + if self._getShowWatchlist(): + self.addShowToTraktWatchList() + self.updateShows() + + logger.log(u"Getting EpisodeWatchlist", logger.DEBUG) + if self._getEpisodeWatchlist(): + self.removeEpisodeFromTraktWatchList() + self.addEpisodeToTraktWatchList() + self.updateEpisodes() + + def removeEpisodeFromTraktWatchList(self): + + logger.log(u"Start looking if some episode has to be removed from watchlist", logger.DEBUG) + + if not len(self.EpisodeWatchlist): + logger.log(u"No episode found in your watchlist, aborting watchlist update", logger.DEBUG) + return True + + trakt_data = [] + for episode in self.EpisodeWatchlist: + tvdb_id = int(episode['show']['ids']['tvdb']) + tvrage_id = int(episode['show']['ids']['tvrage'] or 0) + newShow = helpers.findCertainShow(sickbeard.showList, [tvdb_id, tvrage_id]) + if newShow is not None: + ep_obj = newShow.getEpisode(int(episode['episode']['season']), int(episode['episode']['number'])) + if ep_obj is not None: + if ep_obj.status != WANTED and ep_obj.status != UNKNOWN and ep_obj.status not in Quality.SNATCHED and ep_obj.status not in Quality.SNATCHED_PROPER: + logger.log(u"Removing episode: Indexer " + str(newShow.indexer) + ", indexer_id " + str(newShow.indexerid) + ", Title " + str(newShow.name) + ", Season " + str(episode['episode']['season']) + ", Episode " + str(episode['episode']['number']) + ", Status " + str(ep_obj.status) + " from Watchlist", logger.DEBUG) + trakt_data.append((ep_obj.season, ep_obj.episode)) + else: + logger.log(u"Episode: Indexer " + str(newShow.indexer) + ", indexer_id " + str(newShow.indexerid) + ", Title " + str(newShow.name) + ", Season " + str(episode['episode']["season"]) + ", Episode" + str(episode['episode']["number"]) + " not in Sickberad ShowList", logger.DEBUG) + continue + else: + logger.log(u"Show: tvdb_id " + str(episode['show']['ids']['tvdb']) + ", Title " + str(episode['show']['title']) + " not in Sickberad ShowList", logger.DEBUG) + continue + + if len(trakt_data): + data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) + notifiers.trakt_notifier.update_watchlist(newShow, data_episode=data, update="remove") + self._getEpisodeWatchlist() + + logger.log(u"Stop looking if some episode has to be removed from watchlist", logger.DEBUG) + + def addEpisodeToTraktWatchList(self): + + if sickbeard.TRAKT_SYNC_WATCHLIST and sickbeard.USE_TRAKT: + logger.log(u"Start looking if some WANTED episode need to be added to watchlist", logger.DEBUG) + + myDB = db.DBConnection() + sql_selection='select tv_shows.indexer, showid, show_name, season, episode from tv_episodes,tv_shows where tv_shows.indexer_id = tv_episodes.showid and tv_episodes.status in ('+','.join([str(x) for x in Quality.SNATCHED + Quality.SNATCHED_PROPER + [WANTED]])+')' + episodes = myDB.select(sql_selection) + if episodes is not None: + trakt_data = [] + for cur_episode in episodes: + newShow = helpers.findCertainShow(sickbeard.showList, int(cur_episode["showid"])) + if not self.check_watchlist(newShow, cur_episode["season"], cur_episode["episode"]): + logger.log(u"Episode: Indexer " + str(cur_episode["indexer"]) + ", indexer_id " + str(cur_episode["showid"])+ ", Title " + str(cur_episode["show_name"]) + " " + str(cur_episode["season"]) + "x" + str(cur_episode["episode"]) + " should be added to watchlist", logger.DEBUG) + trakt_data.append((cur_episode["season"], cur_episode["episode"])) + + if len(trakt_data): + data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) + notifiers.trakt_notifier.update_watchlist(newShow, data_episode=data) + self._getEpisodeWatchlist() + + logger.log(u"Stop looking if some WANTED episode need to be added to watchlist", logger.DEBUG) + + def addShowToTraktWatchList(self): + + if sickbeard.TRAKT_SYNC_WATCHLIST and sickbeard.USE_TRAKT: + logger.log(u"Start looking if some show need to be added to watchlist", logger.DEBUG) + + if sickbeard.showList is not None: + trakt_data = [] + for show in sickbeard.showList: + if not self.check_watchlist(show): + logger.log(u"Show: Indexer " + str(show.indexer) + ", indexer_id " + str(show.indexerid) + ", Title " + str(show.name) + " should be added to watchlist", logger.DEBUG) + trakt_data.append((show.indexer, show.indexerid, show.name, show.startyear)) + + if len(trakt_data): + data = notifiers.trakt_notifier.trakt_show_data_generate(trakt_data) + notifiers.trakt_notifier.update_watchlist(data_show=data) + self._getShowWatchlist() + + logger.log(u"Stop looking if some show need to be added to watchlist", logger.DEBUG) + def updateShows(self): logger.log(u"Starting trakt show watchlist check", logger.DEBUG) - try: - watchlist = self.trakt_api.traktRequest("sync/watchlist/shows") - except (traktException, traktAuthException, traktServerBusy) as e: - logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) - return - - if not len(watchlist): + if not len(self.ShowWatchlist): logger.log(u"No shows found in your watchlist, aborting watchlist update", logger.DEBUG) return - for show in watchlist: + for show in self.ShowWatchlist: indexer = int(sickbeard.TRAKT_DEFAULT_INDEXER) if indexer == 2: indexer_id = int(show["show"]["ids"]["tvrage"]) @@ -181,33 +276,28 @@ class TraktChecker(): """ logger.log(u"Starting trakt episode watchlist check", logger.DEBUG) - try: - watchlist = self.trakt_api.traktRequest("sync/watchlist/episodes") - except (traktException, traktAuthException, traktServerBusy) as e: - logger.log(u"Could not connect to Trakt service: %s" % ex(e), logger.WARNING) - return - - if not len(watchlist): - logger.log(u"No shows found in your watchlist, aborting watchlist update", logger.DEBUG) + if not len(self.EpisodeWatchlist): + logger.log(u"No episode found in your watchlist, aborting episode update", logger.DEBUG) return - for show in watchlist: + managed_show = [] + for show in self.EpisodeWatchlist: indexer = int(sickbeard.TRAKT_DEFAULT_INDEXER) if indexer == 2: indexer_id = int(show["show"]["ids"]["tvrage"]) else: indexer_id = int(show["show"]["ids"]["tvdb"]) - self.addDefaultShow(indexer, indexer_id, show["show"]["title"], SKIPPED) newShow = helpers.findCertainShow(sickbeard.showList, indexer_id) - try: - if newShow and newShow.indexer == indexer: - for episode in show["episode"]: - if newShow is not None: - self.setEpisodeToWanted(newShow, episode["season"], episode["number"]) - else: - self.todoWanted.append((indexer_id, episode["season"], episode["number"])) + if newShow is None: + if indexer_id not in managed_show: + self.addDefaultShow(indexer, indexer_id, show["show"]["title"], SKIPPED) + managed_show.append(indexer_id) + self.todoWanted.append((indexer_id, show['episode']['season'], show['episode']['number'])) + else: + if newShow.indexer == indexer: + self.setEpisodeToWanted(newShow, show['episode']['season'], show['episode']['number']) except TypeError: logger.log(u"Could not parse the output from trakt for " + show["show"]["title"], logger.DEBUG) @@ -270,3 +360,48 @@ class TraktChecker(): for episode in episodes: self.todoWanted.remove(episode) self.setEpisodeToWanted(show, episode[1], episode[2]) + + def check_watchlist (self, show_obj, season=None, episode=None): + + found = False + if episode is not None: + watchlist = self.EpisodeWatchlist + else: + watchlist = self.ShowWatchlist + + for watchlist_el in watchlist: + + trakt_id = sickbeard.indexerApi(show_obj.indexer).config['trakt_id'] + if trakt_id == 'tvdb_id': + indexer_id = int(watchlist_el['show']['ids']["tvdb"]) + else: + indexer_id = int(watchlist_el['show']['ids']["tvrage"]) + + if indexer_id == show_obj.indexerid and season is None and episode is None: + found=True + break + elif indexer_id == show_obj.indexerid and season == watchlist_el['episode']["season"] and episode == watchlist_el['episode']["number"]: + found=True + break + + return found + + def _getShowWatchlist(self): + + try: + self.ShowWatchlist = self.trakt_api.traktRequest("sync/watchlist/shows") + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to trakt service, cannot download Show Watchlist: %s" % ex(e), logger.ERROR) + return False + + return True + + def _getEpisodeWatchlist(self): + + try: + self.EpisodeWatchlist = self.trakt_api.traktRequest("sync/watchlist/episodes") + except (traktException, traktAuthException, traktServerBusy) as e: + logger.log(u"Could not connect to trakt service, cannot download Episode Watchlist: %s" % ex(e), logger.WARNING) + return False + + return True diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 9ba4cc00f0eb6fcddb9b217b076fd00497e2cd90..a7fef0de60bbb6afda2823720eeae2c1cc25697d 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -25,7 +25,6 @@ import re import glob import stat import traceback -import shutil import sickbeard @@ -62,6 +61,11 @@ from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, ARCHIVE from common import NAMING_DUPLICATE, NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_SEPARATED_REPEAT, \ NAMING_LIMITED_EXTEND_E_PREFIXED +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + def dirty_setter(attr_name): def wrapper(self, val): @@ -1031,6 +1035,10 @@ class TVShow(object): except OSError, e: logger.log(u'Unable to %s %s: %s / %s' % (action, self._location, repr(e), str(e)), logger.WARNING) + if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC_WATCHLIST: + logger.log(u"Removing show: indexerid " + str(self.indexerid) + ", Title " + str(self.name) + " from Watchlist", logger.DEBUG) + notifiers.trakt_notifier.update_watchlist(self, update="remove") + def populateCache(self): cache_inst = image_cache.ImageCache() @@ -1269,8 +1277,10 @@ class TVShow(object): anyQualities, bestQualities = Quality.splitQuality(self.quality) # @UnusedVariable if bestQualities: maxBestQuality = max(bestQualities) + minBestQuality = min(bestQualities) else: maxBestQuality = None + minBestQuality = None epStatus, curQuality = Quality.splitCompositeStatus(epStatus) @@ -1284,6 +1294,12 @@ class TVShow(object): return Overview.SNATCHED elif maxBestQuality == None: return Overview.GOOD + # if the want only first match and already have one call it good + elif self.archive_firstmatch and curQuality in bestQualities: + return Overview.GOOD + # if they want only first match and current quality is higher than minimal best quality call it good + elif self.archive_firstmatch and minBestQuality != None and curQuality > minBestQuality: + return Overview.GOOD # if they have one but it's not the best they want then mark it as qual elif curQuality < maxBestQuality: return Overview.QUAL diff --git a/sickbeard/versionChecker.py b/sickbeard/versionChecker.py index a1061049199f2cc755107e46b85aeb2bfb7aabdd..3631d310be9f471e0bea5fa97856c696268803d6 100644 --- a/sickbeard/versionChecker.py +++ b/sickbeard/versionChecker.py @@ -18,7 +18,6 @@ import os import platform -import shutil import subprocess import re import urllib @@ -33,6 +32,12 @@ from sickbeard import logger from sickbeard.exceptions import ex from sickbeard import encodingKludge as ek +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + + class CheckVersion(): """ Version check class meant to run as a thread object with the sr scheduler. diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 88b2c9e62c8ab03c095db8e9ffc85943030dd13b..7faf558189a37bb0bb56cec9a5eb94da87a97768 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -42,7 +42,7 @@ from sickbeard import network_timezones from sickbeard import sbdatetime from sickbeard.providers import newznab, rsstorrent from sickbeard.common import Quality, Overview, statusStrings, qualityPresetStrings, cpu_presets -from sickbeard.common import SNATCHED, UNAIRED, IGNORED, ARCHIVED, WANTED, FAILED +from sickbeard.common import SNATCHED, UNAIRED, IGNORED, ARCHIVED, WANTED, FAILED, SKIPPED from sickbeard.common import SD, HD720p, HD1080p from sickbeard.exceptions import ex from sickbeard.blackandwhitelist import BlackAndWhiteList @@ -1159,7 +1159,7 @@ class Home(WebRoot): if not branchDest: return json.dumps({ "status": "error", 'message': 'branchDest empty' }) try: - response = requests.get("https://raw.githubusercontent.com/SICKRAGETV/SickRage/" + str(branchDest) +"/sickbeard/databases/mainDB.py") + response = requests.get("https://raw.githubusercontent.com/SICKRAGETV/SickRage/" + str(branchDest) +"/sickbeard/databases/mainDB.py", verify=False) response.raise_for_status() match = re.search(r"MAX_DB_VERSION\s=\s(?P<version>\d{2,3})",response.text) branchDestDBversion = int(match.group('version')) @@ -1724,6 +1724,7 @@ class Home(WebRoot): return self._genericMessage("Error", errMsg) segments = {} + trakt_data = [] if eps is not None: sql_l = [] @@ -1771,6 +1772,21 @@ class Home(WebRoot): # mass add to database sql_l.append(epObj.get_sql()) + trakt_data.append((epObj.season, epObj.episode)) + + data = notifiers.trakt_notifier.trakt_episode_data_generate(trakt_data) + + if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC_WATCHLIST: + if int(status) in [WANTED, FAILED]: + logger.log(u"Add episodes, showid: indexerid " + str(showObj.indexerid) + ", Title " + str(showObj.name) + " to Watchlist", logger.DEBUG) + upd = "add" + elif int(status) in [ARCHIVED, IGNORED, SKIPPED ] + Quality.DOWNLOADED: + logger.log(u"Remove episodes, showid: indexerid " + str(showObj.indexerid) + ", Title " + str(showObj.name) + " from Watchlist", logger.DEBUG) + upd = "remove" + + if data: + notifiers.trakt_notifier.update_watchlist(showObj, data_episode=data, update=upd) + if len(sql_l) > 0: myDB = db.DBConnection() myDB.mass_action(sql_l) @@ -3610,7 +3626,7 @@ class ConfigGeneral(Config): sickbeard.save_config() def saveGeneral(self, log_dir=None, log_nr = 5, log_size = 1048576, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, - update_shows_on_start=None, trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, + update_shows_on_start=None, update_shows_on_snatch=None, trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, showupdate_hour=3, web_username=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None, web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, @@ -3635,6 +3651,7 @@ class ConfigGeneral(Config): sickbeard.LOG_NR = log_nr sickbeard.LOG_SIZE = log_size sickbeard.UPDATE_SHOWS_ON_START = config.checkbox_to_value(update_shows_on_start) + sickbeard.UPDATE_SHOWS_ON_SNATCH = config.checkbox_to_value(update_shows_on_snatch) sickbeard.TRASH_REMOVE_SHOW = config.checkbox_to_value(trash_remove_show) sickbeard.TRASH_ROTATE_LOGS = config.checkbox_to_value(trash_rotate_logs) config.change_UPDATE_FREQUENCY(update_frequency) @@ -4172,7 +4189,7 @@ class ConfigProviders(Config): return '1' - def canAddTorrentRssProvider(self, name, url, cookies): + def canAddTorrentRssProvider(self, name, url, cookies, titleTAG): if not name: return json.dumps({'error': 'Invalid name specified'}) @@ -4180,7 +4197,7 @@ class ConfigProviders(Config): providerDict = dict( zip([x.getID() for x in sickbeard.torrentRssProviderList], sickbeard.torrentRssProviderList)) - tempProvider = rsstorrent.TorrentRssProvider(name, url, cookies) + tempProvider = rsstorrent.TorrentRssProvider(name, url, cookies, titleTAG) if tempProvider.getID() in providerDict: return json.dumps({'error': 'Exists as ' + providerDict[tempProvider.getID()].name}) @@ -4192,7 +4209,7 @@ class ConfigProviders(Config): return json.dumps({'error': errMsg}) - def saveTorrentRssProvider(self, name, url, cookies): + def saveTorrentRssProvider(self, name, url, cookies, titleTAG): if not name or not url: return '0' @@ -4203,11 +4220,12 @@ class ConfigProviders(Config): providerDict[name].name = name providerDict[name].url = config.clean_url(url) providerDict[name].cookies = cookies + providerDict[name].titleTAG = titleTAG return providerDict[name].getID() + '|' + providerDict[name].configStr() else: - newProvider = rsstorrent.TorrentRssProvider(name, url, cookies) + newProvider = rsstorrent.TorrentRssProvider(name, url, cookies, titleTAG) sickbeard.torrentRssProviderList.append(newProvider) return newProvider.getID() + '|' + newProvider.configStr() @@ -4309,10 +4327,10 @@ class ConfigProviders(Config): if not curTorrentRssProviderStr: continue - curName, curURL, curCookies = curTorrentRssProviderStr.split('|') + curName, curURL, curCookies, curTitleTAG = curTorrentRssProviderStr.split('|') curURL = config.clean_url(curURL) - newProvider = rsstorrent.TorrentRssProvider(curName, curURL, curCookies) + newProvider = rsstorrent.TorrentRssProvider(curName, curURL, curCookies, curTitleTAG) curID = newProvider.getID() @@ -4321,6 +4339,7 @@ class ConfigProviders(Config): torrentRssProviderDict[curID].name = curName torrentRssProviderDict[curID].url = curURL torrentRssProviderDict[curID].cookies = curCookies + torrentRssProviderDict[curID].curTitleTAG = curTitleTAG else: sickbeard.torrentRssProviderList.append(newProvider) @@ -4568,7 +4587,7 @@ class ConfigNotifications(Config): use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, use_trakt=None, trakt_username=None, trakt_password=None, - trakt_remove_watchlist=None, trakt_use_watchlist=None, trakt_method_add=None, + trakt_remove_watchlist=None, trakt_sync_watchlist=None, trakt_method_add=None, trakt_start_paused=None, trakt_use_recommended=None, trakt_sync=None, trakt_default_indexer=None, trakt_remove_serieslist=None, trakt_disable_ssl_verify=None, trakt_timeout=None, use_synologynotifier=None, synologynotifier_notify_onsnatch=None, @@ -4686,7 +4705,7 @@ class ConfigNotifications(Config): sickbeard.TRAKT_PASSWORD = trakt_password sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist) sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist) - sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist) + sickbeard.TRAKT_SYNC_WATCHLIST = config.checkbox_to_value(trakt_sync_watchlist) sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add) sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused) sickbeard.TRAKT_USE_RECOMMENDED = config.checkbox_to_value(trakt_use_recommended) @@ -4878,7 +4897,7 @@ class ErrorLogs(WebRoot): return t.respond() def haveErrors(self): - if len(classes.ErrorViewer.errors) > 0 and sickbeard.GIT_USERNAME and sickbeard.GIT_PASSWORD and sickbeard.GIT_AUTOISSUES == 1: + if len(classes.ErrorViewer.errors) > 0: return True def clearerrors(self): @@ -4886,7 +4905,50 @@ class ErrorLogs(WebRoot): return self.redirect("/errorlogs/") def viewlog(self, minLevel=logger.INFO, logFilter="<NONE>",logSearch=None, maxLines=500): + + def Get_Data(Levelmin, data_in, lines_in, regex, Filter, Search, mlines): + + lastLine = False + numLines = lines_in + numToShow = min(maxLines, numLines + len(data_in)) + + finalData = [] + + for x in reversed(data_in): + + x = ek.ss(x) + match = re.match(regex, x) + + if match: + level = match.group(7) + logName = match.group(8) + if level not in logger.reverseNames: + lastLine = False + continue + if logSearch and logSearch.lower() in x.lower(): + lastLine = True + finalData.append(x) + numLines += 1 + elif not logSearch and logger.reverseNames[level] >= minLevel and (logFilter == '<NONE>' or logName.startswith(logFilter)): + lastLine = True + finalData.append(x) + numLines += 1 + else: + lastLine = False + continue + + elif lastLine: + finalData.append("AA" + x) + numLines += 1 + + + + if numLines >= numToShow: + return finalData + + return finalData + t = PageTemplate(rh=self, file="viewlogs.tmpl") t.submenu = self.ErrorLogsMenu() @@ -4913,51 +4975,24 @@ class ErrorLogs(WebRoot): if logFilter not in logNameFilters: logFilter = '<NONE>' - + regex = "^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$" + data = [] + if os.path.isfile(logger.logFile): with ek.ek(codecs.open, *[logger.logFile, 'r', 'utf-8']) as f: - data = f.readlines() - - regex = "^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$" - - finalData = [] + data = Get_Data(minLevel, f.readlines(), 0, regex, logFilter, logSearch, maxLines) + + for i in range (1 , int(sickbeard.LOG_NR)): + if os.path.isfile(logger.logFile + "." + str(i)) and (len(data) <= maxLines): + with ek.ek(codecs.open, *[logger.logFile + "." + str(i), 'r', 'utf-8']) as f: + data += Get_Data(minLevel, f.readlines(), len(data), regex, logFilter, logSearch, maxLines) - numLines = 0 - lastLine = False - numToShow = min(maxLines, len(data)) - - for x in reversed(data): - - x = ek.ss(x) - match = re.match(regex, x) - - if match: - level = match.group(7) - logName = match.group(8) - if level not in logger.reverseNames: - lastLine = False - continue - - if logSearch and logSearch.lower() in x.lower(): - lastLine = True - finalData.append(x) - elif not logSearch and logger.reverseNames[level] >= minLevel and (logFilter == '<NONE>' or logName.startswith(logFilter)): - lastLine = True - finalData.append(x) - else: - lastLine = False - continue - - elif lastLine: - finalData.append("AA" + x) - - numLines += 1 + - if numLines >= numToShow: - break + - result = "".join(finalData) + result = "".join(data) t.logLines = result t.minLevel = minLevel @@ -4969,6 +5004,7 @@ class ErrorLogs(WebRoot): def submit_errors(self): if not (sickbeard.GIT_USERNAME and sickbeard.GIT_PASSWORD): + ui.notifications.error("Missing information", "Please set your GitHub username and password in the config.") logger.log(u'Please set your GitHub username and password in the config, unable to submit issue ticket to GitHub!') else: issue = logger.submit_errors() diff --git a/tests/test_lib.py b/tests/test_lib.py index 7f152882d1ab7f4c655124a9764ea849ae43f1c2..65c6e406a692099a2057c7aa935debbf1925a135 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -31,7 +31,6 @@ sys.path.append(os.path.abspath('..')) sys.path.append(os.path.abspath('../lib')) import sickbeard -import shutil from sickbeard import providers, tvcache from sickbeard import db @@ -39,6 +38,11 @@ from sickbeard.databases import mainDB from sickbeard.databases import cache_db, failed_db from sickbeard.tv import TVEpisode +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom + #================= # test globals #================= diff --git a/updater.py b/updater.py index 3ce3d6247aa3bf8564799ad431ea0b611affc8e0..7d3ae8b89dce3599b1b27f6cd007677dac1334f9 100644 --- a/updater.py +++ b/updater.py @@ -1,4 +1,9 @@ -import subprocess, os, time, sys, os.path, shutil, re +import subprocess, os, time, sys, os.path, re + +import shutil +import lib.shutil_custom + +shutil.copyfile = lib.shutil_custom.copyfile_custom try: log_file = open('sb-update.log', 'w')