From fe1aabca00ec614732ca497b13d8a4deb436c599 Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Wed, 24 Aug 2016 21:02:12 +0200 Subject: [PATCH 1/2] Change overhaul and add API functions Change API version... start with 10 Change set application response header to 'SickGear' + add API version Change return timezone (of network) in API Add indexer to calls Add SickGear Command tip for old SickBeard commands Add warning old sickbeard API calls only support tvdb shows Add "tvdbid" fallback only for sickbeard calls Add listcommands Add list of all commands (old + new) in listcommand page at the beginning Change hide 'listcommands' command from commands list, since it needs the API builder CSS + is html not json Add missing help in webapi Add episode info: absolute_number, scene_season, scene_episode, scene_absolute_number Add fork to SB command Add sg Add sg.activatescenenumbering Add sg.addrootdir Add sg.checkscheduler Add sg.deleterootdir Add sg.episode Add sg.episode.search Add sg.episode.setstatus Add sg.episode.subtitlesearch Add sg.exceptions Add sg.forcesearch Add sg.future Add sg.getdefaults Add sg.getindexericon Add sg.getindexers to list all indexers Add sg.getmessages Add sg.getnetworkicon Add sg.getrootdirs Add sg.getqualities Add sg.getqualitystrings Add sg.history Add sg.history.clear Add sg.history.trim Add sg.listtraktaccounts Add sg.listignorewords Add sg.listrequiedwords Add sg.logs Add sg.pausebacklog Add sg.postprocess Add sg.ping Add sg.restart Add sg.searchqueue Add sg.searchtv to search all indexers Add sg.setexceptions Add sg.setignorewords Add sg.setrequiredwords Add sg.setscenenumber Add sg.show Add sg.show.addexisting Add sg.show.addnew Add sg.show.cache Add sg.show.delete Add sg.show.getbanner Add sg.show.getfanart Add sg.show.getposter Add sg.show.getquality Add sg.show.listfanart Add sg.show.ratefanart Add sg.show.seasonlist Add sg.show.seasons Add sg.show.setquality Add sg.show.stats Add sg.show.refresh Add sg.show.pause Add sg.show.update Add sg.shows Add sg.shows.browsetrakt Add sg.shows.forceupdate Add sg.shows.queue Add sg.shows.stats Change sickbeard to sickgear Change sickbeard_call to property Change sg.episode.setstatus allow setting of quality Change sg.history, history command output Change sg.searchtv to list of indexers Add uhd4kweb to qualities Add upgrade_once to add existing shows Add upgrade_once to add new show Add upgrade_once to show quality settings (get/set) Add 'ids' to Show + Shows Add ids to coming eps + get tvdb id from ids Add 'status_str' to coming eps Add 'local_datetime' to comming eps + runtime Add X-Filename response header to getbanner, getposter Add X-Fanartname response header for sg.show.getfanart Add missing fields to sb.show Add missing fields to sb.shows Change sb.seasons Change overview optional Change make overview optional in shows Add setscenenumber to API builder Change move set_scene_numbering_helper into scnene_numbering for use in web interface and API Change use quality_map instead of fixed list Add eigthlevel for API/builder page Change limit indexer param to valid values Fix wrong parameter in existing apiBuilder.tmpl that prevents javascript from continuing + add console error message for it Fixed: filter missed shows correctly Add @gen.coroutine --- CHANGES.md | 97 +- gui/slick/interfaces/default/apiBuilder.tmpl | 331 ++- .../interfaces/default/config_general.tmpl | 1 + gui/slick/js/apibuilder.js | 24 +- sickbeard/indexers/indexer_api.py | 5 + sickbeard/network_timezones.py | 18 +- sickbeard/sbdatetime.py | 4 +- sickbeard/scene_numbering.py | 63 + sickbeard/show_queue.py | 8 +- sickbeard/webapi.py | 2536 ++++++++++++++--- sickbeard/webserve.py | 156 +- 11 files changed, 2712 insertions(+), 531 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 22e50652..d92326dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,99 @@ -### 0.14.0 (2018-02-01 02:30:00 UTC) +### 0.15.0 (2018-xx-xx xx:xx:xx UTC) + +* Change overhaul and add API functions +* Change API version... start with 10 +* Change set application response header to 'SickGear' + add API version +* Change return timezone (of network) in API +* Add indexer to calls +* Add SickGear Command tip for old SickBeard commands +* Add warning old sickbeard API calls only support tvdb shows +* Add "tvdbid" fallback only for sickbeard calls +* Add listcommands +* Add list of all commands (old + new) in listcommand page at the beginning +* Change hide 'listcommands' command from commands list, since it needs the API builder CSS + is html not json +* Add missing help in webapi +* Add episode info: absolute_number, scene_season, scene_episode, scene_absolute_number +* Add fork to SB command +* Add sg +* Add sg.activatescenenumbering +* Add sg.addrootdir +* Add sg.checkscheduler +* Add sg.deleterootdir +* Add sg.episode +* Add sg.episode.search +* Add sg.episode.setstatus +* Add sg.episode.subtitlesearch +* Add sg.exceptions +* Add sg.forcesearch +* Add sg.future +* Add sg.getdefaults +* Add sg.getindexericon +* Add sg.getindexers to list all indexers +* Add sg.getmessages +* Add sg.getnetworkicon +* Add sg.getrootdirs +* Add sg.getqualities +* Add sg.getqualitystrings +* Add sg.history +* Add sg.history.clear +* Add sg.history.trim +* Add sg.listtraktaccounts +* Add sg.listignorewords +* Add sg.listrequiedwords +* Add sg.logs +* Add sg.pausebacklog +* Add sg.postprocess +* Add sg.ping +* Add sg.restart +* Add sg.searchqueue +* Add sg.searchtv to search all indexers +* Add sg.setexceptions +* Add sg.setignorewords +* Add sg.setrequiredwords +* Add sg.setscenenumber +* Add sg.show +* Add sg.show.addexisting +* Add sg.show.addnew +* Add sg.show.cache +* Add sg.show.delete +* Add sg.show.getbanner +* Add sg.show.getfanart +* Add sg.show.getposter +* Add sg.show.getquality +* Add sg.show.listfanart +* Add sg.show.ratefanart +* Add sg.show.seasonlist +* Add sg.show.seasons +* Add sg.show.setquality +* Add sg.show.stats +* Add sg.show.refresh +* Add sg.show.pause +* Add sg.show.update +* Add sg.shows +* Add sg.shows.browsetrakt +* Add sg.shows.forceupdate +* Add sg.shows.queue +* Add sg.shows.stats +* Change sickbeard to sickgear +* Change sickbeard_call to property +* Change sg.episode.setstatus allow setting of quality +* Change sg.history, history command output +* Change sg.searchtv to list of indexers +* Add uhd4kweb to qualities +* Add upgrade_once to add existing shows +* Add upgrade_once to add new show +* Add upgrade_once to show quality settings (get/set) +* Add 'ids' to Show + Shows +* Add ids to coming eps + get tvdb id from ids +* Add 'status_str' to coming eps +* Add 'local_datetime' to comming eps + runtime +* Add X-Filename response header to getbanner, getposter +* Add X-Fanartname response header for sg.show.getfanart + +[develop changelog] + + +### 0.14.0 (2018-02-01 02:30:00 UTC) * Change improve core scheduler logic * Change improve media process to parse anime format 'Show Name 123 - 001 - Ep 1 name' diff --git a/gui/slick/interfaces/default/apiBuilder.tmpl b/gui/slick/interfaces/default/apiBuilder.tmpl index 44fb1233..0f3232c9 100644 --- a/gui/slick/interfaces/default/apiBuilder.tmpl +++ b/gui/slick/interfaces/default/apiBuilder.tmpl @@ -3,7 +3,9 @@ API Builder - - +
@@ -372,9 +596,10 @@ addOption("show.pause-opt", "Pause", "&pause=1"); +
- +
diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl index 9425f486..20fb0697 100644 --- a/gui/slick/interfaces/default/config_general.tmpl +++ b/gui/slick/interfaces/default/config_general.tmpl @@ -482,6 +482,7 @@

permit the use of the SickGear (SickBeard) API

+

Old Sickbeard API calls are limited to shows from thetvdb.com. Use new API LINK for full access

diff --git a/gui/slick/js/apibuilder.js b/gui/slick/js/apibuilder.js index 8f7afdc3..527fb26f 100644 --- a/gui/slick/js/apibuilder.js +++ b/gui/slick/js/apibuilder.js @@ -10,7 +10,7 @@ var _disable_empty_list=false; var _hide_empty_list=false; -function goListGroup(apikey, L7, L6, L5, L4, L3, L2, L1){ +function goListGroup(apikey, L8, L7, L6, L5, L4, L3, L2, L1){ var GlobalOptions = ""; $('.global').each(function(){ var checked = $(this).prop('checked'); @@ -26,7 +26,7 @@ function goListGroup(apikey, L7, L6, L5, L4, L3, L2, L1){ }); // handle the show.getposter / show.getbanner differently as they return an image and not json - if (L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner") { + if (L1 == "?cmd=sg.getnetworkicon" || L1 == "?cmd=sg.show.getposter" || L1 == "?cmd=sg.show.getbanner" || L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner" || L1 == "?cmd=sg.getindexericon") { var imgcache = sbRoot + "/api/" + apikey + "/" + L1 + L2 + GlobalOptions; var html = imgcache + '

'; $('#apiResponse').html(html); @@ -36,14 +36,24 @@ function goListGroup(apikey, L7, L6, L5, L4, L3, L2, L1){ cache: false, dataType: "html", success: function (img) { - $('#imgcache').attr('src', imgcache); + $('#imgcache').attr('src', imgcache + "&random=" + Math.random() * 100000000000000000000); } }) } + else if (L1 == "?cmd=listcommands") + { + var html = $.ajax({ + url: sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions, + async: false, + dataType: "html", + }).responseText; + + $('#apiResponse').html(html); + } else { - var html = sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + GlobalOptions + "
";
+        var html = sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions + "
";
         html += $.ajax({
-          url: sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + GlobalOptions,
+          url: sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions,
           async: false,
           dataType: "html",
         }).responseText;
@@ -167,7 +177,7 @@ function cs_addL(dis,link,label,css) { this.items[this.items.length]=new cs_link
 function cs_addG(label,css) { this.items[this.items.length]=new cs_groupOBJ(label,css); }
 function cs_endG() { this.items[this.items.length]=new cs_groupOBJ2(); }
 
-function cs_showMsg(msg) { window.status=msg; }
+function cs_showMsg(msg) { console.error(msg); window.status=msg; }
 function cs_badContent(n) { cs_goodContent=false; cs_showMsg("["+n+"] Not Found."); }
 
 function _setCookie(name, value) {
@@ -636,6 +646,6 @@ function selectOptions(n,opts,mode) {
         }
       }
     }
-  }  
+  }
 }
 // ------
diff --git a/sickbeard/indexers/indexer_api.py b/sickbeard/indexers/indexer_api.py
index b1057514..8db5af17 100644
--- a/sickbeard/indexers/indexer_api.py
+++ b/sickbeard/indexers/indexer_api.py
@@ -125,6 +125,11 @@ class indexerApi(object):
     def indexers(self):
         return dict((int(x['id']), x['name']) for x in indexerConfig.values() if not x['mapped_only'])
 
+    @property
+    def search_indexers(self):
+        return dict((int(x['id']), x['name']) for x in indexerConfig.values() if not x['mapped_only'] and
+                    x.get('active') and not x.get('defunct'))
+
     @property
     def all_indexers(self):
         """
diff --git a/sickbeard/network_timezones.py b/sickbeard/network_timezones.py
index a5076eb8..0972b311 100644
--- a/sickbeard/network_timezones.py
+++ b/sickbeard/network_timezones.py
@@ -295,31 +295,35 @@ def load_network_dict(load=True):
 
 
 # get timezone of a network or return default timezone
-def get_network_timezone(network):
+def get_network_timezone(network, return_name=False):
     if network is None:
         return sb_timezone
 
     timezone = None
+    timezone_name = None
 
     try:
         if zoneinfo.ZONEFILENAME is not None:
             if not network_dict:
                 load_network_dict()
             try:
-                timezone = tz.gettz(network_dupes.get(network) or network_dict.get(network.replace(' ', '').lower()),
-                                    zoneinfo_priority=True)
-            except:
+                timezone_name = network_dupes.get(network) or network_dict.get(network.replace(' ', '').lower())
+                timezone = tz.gettz(timezone_name, zoneinfo_priority=True)
+            except (StandardError, Exception):
                 pass
 
             if timezone is None:
                 cc = re.search(r'\(([a-z]+)\)$', network, flags=re.I)
                 try:
-                    timezone = tz.gettz(country_timezones.get(cc.group(1).upper()), zoneinfo_priority=True)
-                except:
+                    timezone_name = country_timezones.get(cc.group(1).upper())
+                    timezone = tz.gettz(timezone_name, zoneinfo_priority=True)
+                except (StandardError, Exception):
                     pass
-    except:
+    except (StandardError, Exception):
         pass
 
+    if return_name:
+        return timezone if isinstance(timezone, datetime.tzinfo) else sb_timezone, timezone_name
     return timezone if isinstance(timezone, datetime.tzinfo) else sb_timezone
 
 
diff --git a/sickbeard/sbdatetime.py b/sickbeard/sbdatetime.py
index 34bdab13..3566e64c 100644
--- a/sickbeard/sbdatetime.py
+++ b/sickbeard/sbdatetime.py
@@ -113,10 +113,10 @@ class sbdatetime(datetime.datetime):
                     'july', 'august', 'september', 'october', 'november', 'december'])
 
     @static_or_instance
-    def convert_to_setting(self, dt=None):
+    def convert_to_setting(self, dt=None, force_local=False):
         obj = (dt, self)[self is not None]
         try:
-            if 'local' == sickbeard.TIMEZONE_DISPLAY:
+            if force_local or 'local' == sickbeard.TIMEZONE_DISPLAY:
                 return obj.astimezone(sb_timezone)
         except (StandardError, Exception):
             pass
diff --git a/sickbeard/scene_numbering.py b/sickbeard/scene_numbering.py
index f4a8988b..4a2da326 100644
--- a/sickbeard/scene_numbering.py
+++ b/sickbeard/scene_numbering.py
@@ -694,3 +694,66 @@ def fix_xem_numbering(indexer_id, indexer):
     if 0 < len(cl):
         my_db = db.DBConnection()
         my_db.mass_action(cl)
+
+
+def set_scene_numbering_helper(indexerid, indexer, forSeason=None, forEpisode=None, forAbsolute=None,
+                               sceneSeason=None, sceneEpisode=None, sceneAbsolute=None):
+    # sanitize:
+    indexerid = None if indexerid in [None, 'null', ''] else int(indexerid)
+    indexer = None if indexer in [None, 'null', ''] else int(indexer)
+
+    show_obj = sickbeard.helpers.find_show_by_id(sickbeard.showList, {indexer: indexerid}, no_mapped_ids=True)
+
+    if not show_obj:
+        result = {'success': False}
+        return result
+
+    if not show_obj.is_anime:
+        for_season = None if forSeason in [None, 'null', ''] else int(forSeason)
+        for_episode = None if forEpisode in [None, 'null', ''] else int(forEpisode)
+        scene_season = None if sceneSeason in [None, 'null', ''] else int(sceneSeason)
+        scene_episode = None if sceneEpisode in [None, 'null', ''] else int(sceneEpisode)
+        action_log = u'Set episode scene numbering to %sx%s for episode %sx%s of "%s"' \
+                     % (scene_season, scene_episode, for_season, for_episode, show_obj.name)
+        ep_args = {'show': indexerid, 'season': for_season, 'episode': for_episode}
+        scene_args = {'indexer_id': indexerid, 'indexer': indexer, 'season': for_season, 'episode': for_episode,
+                      'sceneSeason': scene_season, 'sceneEpisode': scene_episode}
+        result = {'forSeason': for_season, 'forEpisode': for_episode, 'sceneSeason': None, 'sceneEpisode': None}
+    else:
+        for_absolute = None if forAbsolute in [None, 'null', ''] else int(forAbsolute)
+        scene_absolute = None if sceneAbsolute in [None, 'null', ''] else int(sceneAbsolute)
+        action_log = u'Set absolute scene numbering to %s for episode %s of "%s"' \
+                     % (scene_absolute, for_absolute, show_obj.name)
+        ep_args = {'show': indexerid, 'absolute': for_absolute}
+        scene_args = {'indexer_id': indexerid, 'indexer': indexer, 'absolute_number': for_absolute,
+                      'sceneAbsolute': scene_absolute}
+        result = {'forAbsolute': for_absolute, 'sceneAbsolute': None}
+
+    if ep_args.get('absolute'):
+        ep_obj = show_obj.getEpisode(absolute_number=int(ep_args['absolute']))
+    elif None is not ep_args['season'] and None is not ep_args['episode']:
+        ep_obj = show_obj.getEpisode(int(ep_args['season']), int(ep_args['episode']))
+    else:
+        ep_obj = 'Invalid paramaters'
+
+    if ep_obj is None:
+        ep_obj = "Episode couldn't be retrieved"
+
+    result['success'] = not isinstance(ep_obj, str)
+    if result['success']:
+        logger.log(action_log, logger.DEBUG)
+        set_scene_numbering(**scene_args)
+        show_obj.flushEpisodes()
+    else:
+        result['errorMessage'] = ep_obj
+
+    if not show_obj.is_anime:
+        scene_numbering = get_scene_numbering(indexerid, indexer, for_season, for_episode)
+        if scene_numbering:
+            (result['sceneSeason'], result['sceneEpisode']) = scene_numbering
+    else:
+        scene_numbering = get_scene_absolute_numbering(indexerid, indexer, for_absolute)
+        if scene_numbering:
+            result['sceneAbsolute'] = scene_numbering
+
+    return result
\ No newline at end of file
diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py
index 4110a3d8..d7500bae 100644
--- a/sickbeard/show_queue.py
+++ b/sickbeard/show_queue.py
@@ -177,10 +177,10 @@ class ShowQueue(generic_queue.GenericQueue):
 
     def addShow(self, indexer, indexer_id, showDir, default_status=None, quality=None, flatten_folders=None,
                 lang='en', subtitles=None, anime=None, scene=None, paused=None, blacklist=None, whitelist=None,
-                wanted_begin=None, wanted_latest=None, tag=None, new_show=False, show_name=None):
+                wanted_begin=None, wanted_latest=None, tag=None, new_show=False, show_name=None, upgrade_once=False):
         queueItemObj = QueueItemAdd(indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang,
                                     subtitles, anime, scene, paused, blacklist, whitelist,
-                                    wanted_begin, wanted_latest, tag, new_show=new_show, show_name=show_name)
+                                    wanted_begin, wanted_latest, tag, new_show=new_show, show_name=show_name, upgrade_once=upgrade_once)
 
         self.add_item(queueItemObj)
 
@@ -238,7 +238,7 @@ class ShowQueueItem(generic_queue.QueueItem):
 class QueueItemAdd(ShowQueueItem):
     def __init__(self, indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime,
                  scene, paused, blacklist, whitelist, default_wanted_begin, default_wanted_latest, tag,
-                 scheduled_update=False, new_show=False, show_name=None):
+                 scheduled_update=False, new_show=False, show_name=None, upgrade_once=False):
 
         self.indexer = indexer
         self.indexer_id = indexer_id
@@ -247,6 +247,7 @@ class QueueItemAdd(ShowQueueItem):
         self.default_wanted_begin = default_wanted_begin
         self.default_wanted_latest = default_wanted_latest
         self.quality = quality
+        self.upgrade_once = upgrade_once
         self.flatten_folders = flatten_folders
         self.lang = lang
         self.subtitles = subtitles
@@ -341,6 +342,7 @@ class QueueItemAdd(ShowQueueItem):
             self.show.location = self.showDir
             self.show.subtitles = self.subtitles if None is not self.subtitles else sickbeard.SUBTITLES_DEFAULT
             self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT
+            self.show.archive_firstmatch = self.upgrade_once
             self.show.flatten_folders = self.flatten_folders if None is not self.flatten_folders else sickbeard.FLATTEN_FOLDERS_DEFAULT
             self.show.anime = self.anime if None is not self.anime else sickbeard.ANIME_DEFAULT
             self.show.scene = self.scene if None is not self.scene else sickbeard.SCENE_DEFAULT
diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py
index b7c5ffae..82de8610 100644
--- a/sickbeard/webapi.py
+++ b/sickbeard/webapi.py
@@ -27,6 +27,10 @@ import re
 import traceback
 import sickbeard
 import webserve
+import glob
+
+from mimetypes import MimeTypes
+from random import randint
 
 from sickbeard import db, logger, exceptions, history, ui, helpers
 from sickbeard import encodingKludge as ek
@@ -35,12 +39,16 @@ from sickbeard import image_cache
 from sickbeard import classes
 from sickbeard import processTV
 from sickbeard import network_timezones, sbdatetime
-from sickbeard.exceptions import ex
+from sickbeard.exceptions import ex, MultipleShowObjectsException
 from sickbeard.common import SNATCHED, SNATCHED_ANY, SNATCHED_PROPER, SNATCHED_BEST, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED, UNKNOWN
 from sickbeard.helpers import remove_article
+from sickbeard.scene_numbering import set_scene_numbering_helper
 from common import Quality, qualityPresetStrings, statusStrings
 from sickbeard.indexers.indexer_config import *
-from sickbeard.webserve import MainHandler
+from sickbeard.indexers import indexer_config, indexer_api
+from tornado import gen
+from sickbeard.search_backlog import FORCED_BACKLOG
+from sickbeard.webserve import NewHomeAddShows
 
 try:
     import json
@@ -49,6 +57,7 @@ except ImportError:
 
 from lib import subliminal
 
+
 dateFormat = "%Y-%m-%d"
 dateTimeFormat = "%Y-%m-%d %H:%M"
 timeFormat = '%A %I:%M %p'
@@ -68,10 +77,24 @@ result_type_map = {RESULT_SUCCESS: "success",
 }
 # basically everything except RESULT_SUCCESS / success is bad
 
+quality_map = {'sdtv': Quality.SDTV,
+               'sddvd': Quality.SDDVD,
+               'hdtv': Quality.HDTV,
+               'rawhdtv': Quality.RAWHDTV,
+               'fullhdtv': Quality.FULLHDTV,
+               'hdwebdl': Quality.HDWEBDL,
+               'fullhdwebdl': Quality.FULLHDWEBDL,
+               'hdbluray': Quality.HDBLURAY,
+               'fullhdbluray': Quality.FULLHDBLURAY,
+               'uhd4kweb': Quality.UHD4KWEB,
+               'unknown': Quality.UNKNOWN}
+
+quality_map_inversed = {v: k for k, v in quality_map.iteritems()}
+
 
 class Api(webserve.BaseHandler):
     """ api class that returns json results """
-    version = 4  # use an int since float-point is unpredictible
+    version = 10  # use an int since float-point is unpredictible
     intent = 4
 
     def set_default_headers(self):
@@ -79,7 +102,10 @@ class Api(webserve.BaseHandler):
         self.set_header('X-Robots-Tag', 'noindex, nofollow, noarchive, nocache, noodp, noydir, noimageindex, nosnippet')
         if sickbeard.SEND_SECURITY_HEADERS:
             self.set_header('X-Frame-Options', 'SAMEORIGIN')
+        self.set_header('X-Application', 'SickGear')
+        self.set_header('X-API-Version', Api.version)
 
+    @gen.coroutine
     def get(self, route, *args, **kwargs):
         route = route.strip('/') or 'index'
 
@@ -287,6 +313,19 @@ class ApiCall(object):
         # RequestHandler
         self.handler = handler
 
+        # old sickbeard call
+        self._sickbeard_call = getattr(self, '_sickbeard_call', False)
+
+    @property
+    def sickbeard_call(self):
+        if hasattr(self, '_sickbeard_call'):
+            return self._sickbeard_call
+        return False
+
+    @sickbeard_call.setter
+    def sickbeard_call(self, v):
+        self._sickbeard_call = v
+
     def run(self):
         # override with real output function in subclass
         return {}
@@ -335,13 +374,13 @@ class ApiCall(object):
             msg = "The required parameters: '" + "','".join(self._missing) + "' where not set"
         return _responds(RESULT_ERROR, msg=msg)
 
-    def check_params(self, args, kwargs, key, default, required, type, allowedValues):
+    def check_params(self, args, kwargs, key, default, required, type, allowedValues, sub_type=None):
         # TODO: explain this
         """ function to check passed params for the shorthand wrapper
             and to detect missing/required param
         """
         # Fix for applications that send tvdbid instead of indexerid
-        if key == "indexerid" and "indexerid" not in kwargs:
+        if self.sickbeard_call and key == "indexerid" and "indexerid" not in kwargs:
             key = "tvdbid"
 
         missing = True
@@ -360,7 +399,8 @@ class ApiCall(object):
         if required:
             try:
                 self._missing
-                self._requiredParams.append(key)
+                self._requiredParams[key] = {"allowedValues": allowedValues,
+                                             "defaultValue": orgDefault}
             except AttributeError:
                 self._missing = []
                 self._requiredParams = {}
@@ -378,14 +418,14 @@ class ApiCall(object):
                                              "defaultValue": orgDefault}
 
         if default:
-            default = self._check_param_type(default, key, type)
+            default = self._check_param_type(default, key, type, sub_type)
             if type == "bool":
                 type = []
             self._check_param_value(default, key, allowedValues)
 
         return default, args
 
-    def _check_param_type(self, value, name, type):
+    def _check_param_type(self, value, name, type, sub_type):
         """ checks if value can be converted / parsed to type
             will raise an error on failure
             or will convert it to type and return new converted value
@@ -412,7 +452,29 @@ class ApiCall(object):
             else:
                 error = True
         elif type == "list":
-            value = value.split("|")
+            if None is not sub_type:
+                if sub_type in (int, long):
+                    if isinstance(value, (int, long)):
+                        value = [value]
+                    elif isinstance(value, basestring):
+                        if '|' in value:
+                            li = [int(v) for v in value.split('|')]
+                            if any([not isinstance(v, (int, long)) for v in li]):
+                                error = True
+                            else:
+                                value = li
+                        else:
+                            value = [int(value)]
+                    else:
+                        error = True
+                else:
+                    li = value.split('|')
+                    if any([sub_type is not type(v) for v in li]):
+                        error = True
+                    else:
+                        value = li
+            else:
+                value = value.split("|")
         elif type == "string":
             pass
         elif type == "ignore":
@@ -589,17 +651,7 @@ def _mapQuality(showObj):
 
 
 def _getQualityMap():
-    return {Quality.SDTV: 'sdtv',
-            Quality.SDDVD: 'sddvd',
-            Quality.HDTV: 'hdtv',
-            Quality.RAWHDTV: 'rawhdtv',
-            Quality.FULLHDTV: 'fullhdtv',
-            Quality.HDWEBDL: 'hdwebdl',
-            Quality.FULLHDWEBDL: 'fullhdwebdl',
-            Quality.HDBLURAY: 'hdbluray',
-            Quality.FULLHDBLURAY: 'fullhdbluray',
-            Quality.UNKNOWN: 'unknown'}
-
+    return quality_map_inversed
 
 def _getRootDirs():
     if sickbeard.ROOT_DIRS == "":
@@ -651,6 +703,83 @@ class IntParseError(Exception):
 
 # -------------------------------------------------------------------------------------#
 
+class CMD_ListCommands(ApiCall):
+    _help = {"desc": "list help of all commands",
+    }
+
+    def __init__(self, handler, args, kwargs):
+        # required
+        # optional
+        ApiCall.__init__(self, handler, args, kwargs)
+
+    def run(self):
+        """ display help information for all commands """
+        out = ''
+        table_sickgear_commands = ''
+        table_sickbeard_commands = ''
+        for f, v in sorted(_functionMaper.iteritems(), key=lambda x: (re.sub(r'^s[bg]\.', '', x[0], flags=re.I), re.sub(r'^sg\.', '1', x[0], flags=re.I))):
+            if 'listcommands' == f:
+                continue
+            help = getattr(v, '_help', None)
+            is_old_command = isinstance(help, dict) and "SickGearCommand" in help
+            if is_old_command:
+                table_sickbeard_commands += '
' % f + else: + table_sickgear_commands += '' % f + color = ("", " style='color: grey !important;'")[is_old_command] + out += '

%s%s

' % (color, f, ("", " (Sickbeard compatibility command)")[is_old_command]) + if isinstance(help, dict): + sg_c = '' + if "SickGearCommand" in help: + sg_c += '' % help['SickGearCommand'] + out += "

for all features use SickGear API Command: %s

" % help['SickGearCommand'] + if "desc" in help: + if is_old_command: + table_sickbeard_commands += '%s' % (help['desc'], sg_c) + else: + table_sickgear_commands += '' % help['desc'] + out += help['desc'] + + table = '' + + if "requiredParameters" in help and isinstance(help['requiredParameters'], dict): + for p, d in help['requiredParameters'].iteritems(): + des = '' + if isinstance(d, dict) and 'desc' in d: + des = d.get('desc') + table += "" % (p, des) + + if "optionalParameters" in help and isinstance(help['optionalParameters'], dict): + for p, d in help['optionalParameters'].iteritems(): + des = '' + if isinstance(d, dict) and 'desc' in d: + des = d.get('desc') + table += "" % (p, des) + if table: + out += "
%s
%s%s%s%s
%s required

%s

%s optional

%s

" + out += table + out += '
ParameterDescription
' + else: + if is_old_command: + table_sickbeard_commands += '%s' % 'no description' + else: + table_sickgear_commands += '%s' % 'no description' + + if is_old_command: + table_sickbeard_commands += '' + else: + table_sickgear_commands += '' + + if table_sickbeard_commands: + out = "

SickBeard Commands (compatibility):

" + table_sickbeard_commands + '
CommandDescriptionReplacement SickGear Command
' + out + + if table_sickgear_commands: + out = "

SickGear Commands:

" + table_sickgear_commands + '
CommandDescription
' + out + + return out + class CMD_Help(ApiCall): _help = {"desc": "display help information for a given subject/command", @@ -673,7 +802,7 @@ class CMD_Help(ApiCall): return out -class CMD_ComingEpisodes(ApiCall): +class CMD_SickGearComingEpisodes(ApiCall): _help = {"desc": "display the coming episodes", "optionalParameters": {"sort": {"desc": "change the sort order"}, "type": {"desc": "one or more of allowedValues separated by |"}, @@ -709,28 +838,51 @@ class CMD_ComingEpisodes(ApiCall): myDB = db.DBConnection() sql_results = myDB.select( - "SELECT airdate, airs, episode, name AS 'ep_name', description AS 'ep_plot', network, season, showid AS 'indexerid', show_name, tv_shows.quality AS quality, tv_shows.status AS 'show_status', tv_shows.paused AS 'paused' FROM tv_episodes, tv_shows WHERE season != 0 AND airdate >= ? AND airdate <= ? AND tv_shows.indexer_id = tv_episodes.showid AND tv_episodes.status NOT IN (" + ','.join( - ['?'] * len(qualList)) + ")", [yesterday, next_week] + qualList) + "SELECT airdate, airs, runtime, tv_shows.indexer AS 'indexer', episode, name AS 'ep_name', " + "tv_episodes.status as 'status', description AS 'ep_plot', network, season, showid AS 'indexerid', " + "show_name, tv_shows.quality AS quality, tv_shows.status AS 'show_status', " + "tv_shows.paused AS 'paused' FROM tv_episodes, tv_shows WHERE " + + ("", "tv_shows.indexer = %s AND " % INDEXER_TVDB)[self.sickbeard_call] + + "season != 0 AND airdate >= ? AND " + "airdate <= ? AND tv_shows.indexer_id = tv_episodes.showid AND tv_shows.indexer == tv_episodes.indexer AND " + "tv_episodes.status NOT IN (" + ','.join(['?'] * len(qualList)) + ")", [yesterday, next_week] + qualList) for cur_result in sql_results: - done_show_list.append(int(cur_result["indexerid"])) + done_show_list.append((int(cur_result["indexerid"]), int(cur_result["indexer"]))) - more_sql_results = myDB.select( - "SELECT airdate, airs, episode, name AS 'ep_name', description AS 'ep_plot', network, season, showid AS 'indexerid', show_name, tv_shows.quality AS quality, tv_shows.status AS 'show_status', tv_shows.paused AS 'paused' FROM tv_episodes outer_eps, tv_shows WHERE season != 0 AND showid NOT IN (" + ','.join( - ['?'] * len( - done_show_list)) + ") AND tv_shows.indexer_id = outer_eps.showid AND airdate = (SELECT airdate FROM tv_episodes inner_eps WHERE inner_eps.season != 0 AND inner_eps.showid = outer_eps.showid AND inner_eps.airdate >= ? ORDER BY inner_eps.airdate ASC LIMIT 1) AND outer_eps.status NOT IN (" + ','.join( - ['?'] * len(Quality.DOWNLOADED + Quality.SNATCHED)) + ")", - done_show_list + [next_week] + Quality.DOWNLOADED + Quality.SNATCHED) + more_sql_results = [m for m in myDB.select( + "SELECT airdate, airs, runtime, tv_shows.indexer AS 'indexer', episode, name AS 'ep_name', " + "outer_eps.status as 'status', description AS 'ep_plot', network, season, showid AS 'indexerid', " + "show_name, tv_shows.quality AS quality, tv_shows.status AS 'show_status', " + "tv_shows.paused AS 'paused' FROM tv_episodes outer_eps, tv_shows WHERE " + + ("", "tv_shows.indexer = %s AND " % INDEXER_TVDB)[self.sickbeard_call] + + "season != 0 AND " + "tv_shows.indexer_id = outer_eps.showid AND tv_shows.indexer == outer_eps.indexer AND " + "airdate = (SELECT airdate FROM tv_episodes inner_eps WHERE inner_eps.season != 0 AND " + "inner_eps.showid = outer_eps.showid AND inner_eps.indexer == outer_eps.indexer AND " + "inner_eps.airdate >= ? ORDER BY inner_eps.airdate ASC LIMIT 1) AND " + "outer_eps.status NOT IN (" + ','.join(['?'] * len(Quality.DOWNLOADED + Quality.SNATCHED)) + ")", + [next_week] + Quality.DOWNLOADED + Quality.SNATCHED) if (int(m['indexerid']), int(m['indexer'])) + not in done_show_list] sql_results += more_sql_results more_sql_results = myDB.select( - "SELECT airdate, airs, episode, name AS 'ep_name', description AS 'ep_plot', network, season, showid AS 'indexerid', show_name, tv_shows.quality AS quality, tv_shows.status AS 'show_status', tv_shows.paused AS 'paused' FROM tv_episodes, tv_shows WHERE season != 0 AND tv_shows.indexer_id = tv_episodes.showid AND airdate <= ? AND airdate >= ? AND tv_episodes.status = ? AND tv_episodes.status NOT IN (" + ','.join( + "SELECT airdate, airs, runtime, tv_shows.indexer AS 'indexer', episode, name AS 'ep_name', " + "tv_episodes.status as 'status', description AS 'ep_plot', network, season, showid AS 'indexerid', " + "show_name, tv_shows.quality AS quality, tv_shows.status AS 'show_status', " + "tv_shows.paused AS 'paused' FROM tv_episodes, tv_shows WHERE " + + ("", "tv_shows.indexer = %s AND " % INDEXER_TVDB)[self.sickbeard_call] + + "season != 0 AND tv_shows.indexer_id = tv_episodes.showid AND tv_shows.indexer == tv_episodes.indexer AND " + "airdate <= ? AND airdate >= ? AND " + "tv_episodes.status = ? AND tv_episodes.status NOT IN (" + ','.join( ['?'] * len(qualList)) + ")", [tomorrow, recently, WANTED] + qualList) sql_results += more_sql_results sql_results = list(set(sql_results)) # make a dict out of the sql results - sql_results = [dict(row) for row in sql_results] + sql_results = [dict(row) for row in sql_results + if Quality.splitCompositeStatus(helpers.tryInt(row['status']))[0] not in + [DOWNLOADED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, ARCHIVED, IGNORED, SKIPPED]] # multi dimension sort sorts = { @@ -752,9 +904,15 @@ class CMD_ComingEpisodes(ApiCall): # add parsed_datetime to the dict for index, item in enumerate(sql_results): - sql_results[index]['parsed_datetime'] = network_timezones.parse_date_time(item['airdate'], item['airs'], item['network']) + timezone, sql_results[index]['timezone'] = network_timezones.get_network_timezone(item['network'], + return_name=True) + p_t = network_timezones.parse_date_time(item['airdate'], item['airs'], timezone) + sql_results[index]['parsed_datetime'] = p_t + sql_results[index]['local_datetime'] = sbdatetime.sbdatetime.sbstrftime( + sbdatetime.sbdatetime.convert_to_setting(p_t, force_local=True), dateTimeFormat) sql_results[index]['data_show_name'] = value_maybe_article(item['show_name']) sql_results[index]['data_network'] = value_maybe_article(item['network']) + sql_results[index]['status_str'] = statusStrings[item['status']] sql_results.sort(sorts[self.sort]) @@ -801,7 +959,14 @@ class CMD_ComingEpisodes(ApiCall): # start day of the week on 1 (monday) ep['weekday'] = 1 + datetime.date.fromordinal(ep['airdate']).weekday() # Add tvdbid for backward compability - ep["tvdbid"] = ep['indexerid'] + try: + showObj = helpers.find_show_by_id(sickbeard.showList, {ep['indexer']: ep['indexerid']}) + ep['tvdbid'] = showObj.ids.get(INDEXER_TVDB, {'id': 0})['id'] + ep['ids'] = {k: v.get('id') for k, v in showObj.ids.iteritems()} + except (StandardError, Exception): + ep['tvdbid'] = (None, ep['indexerid'])[INDEXER_TVDB == ep['indexer']] + ep['ids'] = None + ep['airdate'] = sbdatetime.sbdatetime.sbfdate(datetime.date.fromordinal(ep['airdate']), d_preset=dateFormat) ep['parsed_datetime'] = sbdatetime.sbdatetime.sbfdatetime(ep['parsed_datetime'], d_preset=dateFormat, t_preset='%H:%M %z') @@ -814,19 +979,41 @@ class CMD_ComingEpisodes(ApiCall): return _responds(RESULT_SUCCESS, finalEpResults) -class CMD_Episode(ApiCall): - _help = {"desc": "display detailed info about an episode", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - "season": {"desc": "the season number"}, - "episode": {"desc": "the episode number"} - }, - "optionalParameters": {"full_path": { - "desc": "show the full absolute path (if valid) instead of a relative path for the episode location"} - } - } +class CMD_ComingEpisodes(CMD_SickGearComingEpisodes): + _help = {"desc": "display the coming episodes", + "optionalParameters": {"sort": {"desc": "change the sort order"}, + "type": {"desc": "one or more of allowedValues separated by |"}, + "paused": { + "desc": "0 to exclude paused shows, 1 to include them, or omitted to use the SB default"}, + }, + "SickGearCommand": "sg.future", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearComingEpisodes.__init__(self, handler, args, kwargs) + + +class CMD_SickGearEpisode(ApiCall): + _help = {"desc": "display detailed info about an episode", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + "season": {"desc": "the season number"}, + "episode": {"desc": "the episode number"} + }, + "optionalParameters": {"full_path": { + "desc": "show the full absolute path (if valid) instead of a relative path for the episode location"}, + + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) self.s, args = self.check_params(args, kwargs, "season", None, True, "int", []) self.e, args = self.check_params(args, kwargs, "episode", None, True, "int", []) @@ -837,14 +1024,16 @@ class CMD_Episode(ApiCall): def run(self): """ display detailed info about an episode """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") myDB = db.DBConnection(row_type="dict") sqlResults = myDB.select( - "SELECT name, description, airdate, status, location, file_size, release_name, subtitles FROM tv_episodes WHERE showid = ? AND episode = ? AND season = ?", - [self.indexerid, self.e, self.s]) + "SELECT name, description, airdate, status, location, file_size, release_name, subtitles, absolute_number, " + "scene_season, scene_episode, scene_absolute_number FROM tv_episodes WHERE indexer = ? AND showid = ? " + "AND episode = ? AND season = ?", + [self.indexer, self.indexerid, self.e, self.s]) if not len(sqlResults) == 1: raise ApiError("Episode not found") episode = sqlResults[0] @@ -865,7 +1054,8 @@ class CMD_Episode(ApiCall): elif not showPath: # show dir is broken ... episode path will be empty episode["location"] = "" # convert stuff to human form - episode['airdate'] = sbdatetime.sbdatetime.sbfdate(sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(int(episode['airdate']), showObj.airs, showObj.network)), d_preset=dateFormat) + timezone, episode['timezone'] = network_timezones.get_network_timezone(showObj.network, return_name=True) + episode['airdate'] = sbdatetime.sbdatetime.sbfdate(sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(int(episode['airdate']), showObj.airs, timezone)), d_preset=dateFormat) status, quality = Quality.splitCompositeStatus(int(episode["status"])) episode["status"] = _get_status_Strings(status) episode["quality"] = _get_quality_string(quality) @@ -874,16 +1064,39 @@ class CMD_Episode(ApiCall): return _responds(RESULT_SUCCESS, episode) -class CMD_EpisodeSearch(ApiCall): - _help = {"desc": "search for an episode. the response might take some time", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, +class CMD_Episode(CMD_SickGearEpisode): + _help = {"desc": "display detailed info about an episode", + "requiredParameters": {"indexerid": {"desc": "unique id of a show"}, "season": {"desc": "the season number"}, "episode": {"desc": "the episode number"} - } + }, + "optionalParameters": {"full_path": { + "desc": "show the full absolute path (if valid) instead of a relative path for the episode location"}, + }, + "SickGearCommand": "sg.episode", + } + + def __init__(self, handler, args, kwargs): + # required + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearEpisode.__init__(self, handler, args, kwargs) + + +class CMD_SickGearEpisodeSearch(ApiCall): + _help = {"desc": "search for an episode. the response might take some time", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + "season": {"desc": "the season number"}, + "episode": {"desc": "the episode number"} + }, } def __init__(self, handler, args, kwargs): # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) self.s, args = self.check_params(args, kwargs, "season", None, True, "int", []) self.e, args = self.check_params(args, kwargs, "episode", None, True, "int", []) @@ -893,7 +1106,7 @@ class CMD_EpisodeSearch(ApiCall): def run(self): """ search for an episode """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -920,32 +1133,55 @@ class CMD_EpisodeSearch(ApiCall): return _responds(RESULT_FAILURE, msg='Unable to find episode') -class CMD_EpisodeSetStatus(ApiCall): - _help = {"desc": "set status of an episode or season (when no ep is provided)", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, +class CMD_EpisodeSearch(CMD_SickGearEpisodeSearch): + _help = {"desc": "search for an episode. the response might take some time", + "requiredParameters": {"tvdbid": {"desc": "thetvdb.com id of a show"}, "season": {"desc": "the season number"}, - "status": {"desc": "the status values: wanted, skipped, archived, ignored, failed"} - }, - "optionalParameters": {"episode": {"desc": "the episode number"}, - "force": {"desc": "should we replace existing (downloaded) episodes or not"} - } - } + "episode": {"desc": "the episode number"} + }, + "SickGearCommand": "episode.search", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearEpisodeSearch.__init__(self, handler, args, kwargs) + + +class CMD_SickGearEpisodeSetStatus(ApiCall): + _help = {"desc": "set status of an episode or season (when no ep is provided)", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + "season": {"desc": "the season number"}, + "status": {"desc": "the status values: wanted, skipped, archived, ignored, failed, snatched, downloaded"} + }, + "optionalParameters": {"episode": {"desc": "the episode number"}, + "force": {"desc": "should we replace existing (downloaded) episodes or not"}, + "quality": {"desc": "set quality of episode(s), only for statuses: snatched, downloaded, archived"}, + } + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) self.s, args = self.check_params(args, kwargs, "season", None, True, "int", []) self.status, args = self.check_params(args, kwargs, "status", None, True, "string", - ["wanted", "skipped", "archived", "ignored", "failed"]) + ["wanted", "skipped", "archived", "ignored", "failed", "snatched", "downloaded"]) # optional self.e, args = self.check_params(args, kwargs, "episode", None, False, "int", []) self.force, args = self.check_params(args, kwargs, "force", 0, False, "bool", []) + self.quality, args = self.check_params(args, kwargs, "quality", None, False, "string", [q for q in quality_map]) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): """ set status of an episode or a season (when no ep is provided) """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -958,6 +1194,12 @@ class CMD_EpisodeSetStatus(ApiCall): # the allowed values has at least one item that could not be matched against the internal status strings raise ApiError("The status string could not be matched to a status. Report to Devs!") + if None is not self.quality: + if self.status not in (SNATCHED, SNATCHED_BEST, SNATCHED_PROPER, DOWNLOADED, ARCHIVED): + return _responds(RESULT_FAILURE, msg="Can't set status %s together with quailty: %s" % + (statusStrings[self.status], self.quality)) + self.quality = quality_map[self.quality] + ep_list = [] if self.e: epObj = showObj.getEpisode(self.s, self.e) @@ -996,13 +1238,16 @@ class CMD_EpisodeSetStatus(ApiCall): continue # allow the user to force setting the status for an already downloaded episode - if epObj.status in Quality.DOWNLOADED and not self.force: + if epObj.status in Quality.DOWNLOADED and not self.force and None is self.quality: ep_results.append(_epResult(RESULT_FAILURE, epObj, "Refusing to change status because it is already marked as DOWNLOADED")) failure = True continue - epObj.status = self.status + if None is not self.quality: + epObj.status = Quality.compositeStatus(self.status, self.quality) + else: + epObj.status = self.status result = epObj.get_sql() if None is not result: sql_l.append(result) @@ -1032,16 +1277,40 @@ class CMD_EpisodeSetStatus(ApiCall): return _responds(RESULT_SUCCESS, msg='All status set successfully.' + extra_msg) -class CMD_SubtitleSearch(ApiCall): - _help = {"desc": "search episode subtitles. the response might take some time", +class CMD_EpisodeSetStatus(CMD_SickGearEpisodeSetStatus): + _help = {"desc": "set status of an episode or season (when no ep is provided)", "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, + "season": {"desc": "the season number"}, + "status": {"desc": "the status values: wanted, skipped, archived, ignored, failed"} + }, + "optionalParameters": {"episode": {"desc": "the episode number"}, + "force": {"desc": "should we replace existing (downloaded) episodes or not"} + }, + "SickGearCommand": "sg.episode.setstatus", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + kwargs['indexer'] = INDEXER_TVDB + CMD_SickGearEpisodeSetStatus.__init__(self, handler, args, kwargs) + + +class CMD_SickGearSubtitleSearch(ApiCall): + _help = {"desc": "search episode subtitles. the response might take some time", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, "season": {"desc": "the season number"}, "episode": {"desc": "the episode number"} - } + }, } def __init__(self, handler, args, kwargs): # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) self.s, args = self.check_params(args, kwargs, "season", None, True, "int", []) self.e, args = self.check_params(args, kwargs, "episode", None, True, "int", []) @@ -1051,7 +1320,7 @@ class CMD_SubtitleSearch(ApiCall): def run(self): """ search episode subtitles """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -1084,16 +1353,37 @@ class CMD_SubtitleSearch(ApiCall): return response -class CMD_Exceptions(ApiCall): +class CMD_SubtitleSearch(ApiCall): + _help = {"desc": "search episode subtitles. the response might take some time", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, + "season": {"desc": "the season number"}, + "episode": {"desc": "the episode number"} + }, + "SickGearCommand": "sg.episode.subtitlesearch", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + ApiCall.__init__(self, handler, args, kwargs) + + +class CMD_SickGearExceptions(ApiCall): _help = {"desc": "display scene exceptions for all or a given show", - "optionalParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - } - } + "optionalParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + } + } def __init__(self, handler, args, kwargs): # required # optional self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, False, "int", + [i for i in indexer_api.indexerApi().indexers]) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) @@ -1111,7 +1401,7 @@ class CMD_Exceptions(ApiCall): scene_exceptions[indexerid].append(row["show_name"]) else: - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -1125,12 +1415,95 @@ class CMD_Exceptions(ApiCall): return _responds(RESULT_SUCCESS, scene_exceptions) -class CMD_History(ApiCall): - _help = {"desc": "display sickbeard downloaded/snatched history", +class CMD_Exceptions(CMD_SickGearExceptions): + _help = {"desc": "display scene exceptions for all or a given show", + "optionalParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.exceptions", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearExceptions.__init__(self, handler, args, kwargs) + + +class CMD_SetExceptions(ApiCall): + _help = {"desc": "set scene exceptions for a given show", + "requiredParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + "forseason": {"desc": "exception for season, -1 for all seasons"}, + }, + "optionalParameters": {"add": {"desc": "list of exceptions to add"}, + "remove": {"desc": "list of exceptions to remove"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.forseason, args = self.check_params(args, kwargs, "forseason", None, True, "int", []) + # optional + self.add, args = self.check_params(args, kwargs, "add", None, False, "list", []) + self.remove, args = self.check_params(args, kwargs, "remove", None, False, "list", []) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + if not self.add and not self.remove: + return _responds(RESULT_FAILURE, 'No Exceptions provided to be add or removed.') + + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) + if not showObj: + return _responds(RESULT_FAILURE, 'Could not find any show in db from indexer: %s with id: %s' % + (self.indexer, self.indexerid)) + + myDB = db.DBConnection(row_type="dict") + sqlResults = myDB.select("SELECT show_name, season, indexer_id AS 'indexerid' FROM scene_exceptions WHERE " + "indexer_id = ? and season = ?", [self.indexerid, self.forseason]) + + cl = [] + curexep = [(s['show_name'], s['season']) for s in sqlResults] + add_list = [] + remove_list = [] + if self.remove: + for r in self.remove: + if (r, self.forseason) in curexep: + cl.append(['DELETE FROM scene_exceptions WHERE indexer_id = ? AND season = ? AND show_name = ?', + [self.indexerid, self.forseason, r]]) + try: + curexep.remove((r, self.forseason)) + except ValueError: + pass + remove_list.append(r) + + if self.add: + for a in self.add: + if (a, self.forseason) not in curexep: + cl.append(['INSERT INTO scene_exceptions (show_name, indexer_id, season) VALUES (?,?,?)', + [a, self.indexerid, self.forseason]]) + curexep.append((a, self.forseason)) + add_list.append(a) + + if cl: + myDB.mass_action(cl) + return _responds(RESULT_SUCCESS, data={'added': add_list, 'removed': remove_list, 'for season': self.forseason, + 'current': [c[0] for c in curexep], 'indexer': self.indexer, + 'indexerid': self.indexerid}, + msg='Exceptions changed.') + + +class CMD_SickGearHistory(ApiCall): + _help = {"desc": "display sickgear downloaded/snatched history", "optionalParameters": {"limit": {"desc": "limit returned results"}, "type": {"desc": "only show a specific type of results"}, - } - } + } + } def __init__(self, handler, args, kwargs): # required @@ -1141,7 +1514,7 @@ class CMD_History(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ display sickbeard downloaded/snatched history """ + """ display sickgear downloaded/snatched history """ typeCodes = [] if self.type == "downloaded": @@ -1158,12 +1531,15 @@ class CMD_History(ApiCall): ulimit = min(int(self.limit), 100) if ulimit == 0: sqlResults = myDB.select( - "SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.indexer_id AND action in (" + ','.join( - ['?'] * len(typeCodes)) + ") ORDER BY date DESC", typeCodes) + "SELECT h.*, show_name, s.indexer FROM history h, tv_shows s WHERE h.showid=s.indexer_id" + + ("", " AND s.indexer=%s" % INDEXER_TVDB)[self.sickbeard_call] + + " AND action in (" + ','.join(['?'] * len(typeCodes)) + ") ORDER BY date DESC", typeCodes) else: sqlResults = myDB.select( - "SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.indexer_id AND action in (" + ','.join( - ['?'] * len(typeCodes)) + ") ORDER BY date DESC LIMIT ?", typeCodes + [ulimit]) + "SELECT h.*, show_name, s.indexer FROM history h, tv_shows s WHERE h.showid=s.indexer_id" + + ("", " AND s.indexer=%s" % INDEXER_TVDB)[self.sickbeard_call] + + " AND action in (" + ','.join(['?'] * len(typeCodes)) + ") ORDER BY date DESC LIMIT ?", + typeCodes + [ulimit]) results = [] for row in sqlResults: @@ -1179,15 +1555,31 @@ class CMD_History(ApiCall): row["resource_path"] = os.path.dirname(row["resource"]) row["resource"] = os.path.basename(row["resource"]) # Add tvdbid for backward compability - row['tvdbid'] = row['indexerid'] + row['tvdbid'] = (None, row['indexerid'])[INDEXER_TVDB == row['indexer']] results.append(row) return _responds(RESULT_SUCCESS, results) -class CMD_HistoryClear(ApiCall): - _help = {"desc": "clear sickbeard's history", - } +class CMD_History(CMD_SickGearHistory): + _help = {"desc": "display sickgear downloaded/snatched history", + "optionalParameters": {"limit": {"desc": "limit returned results"}, + "type": {"desc": "only show a specific type of results"}, + }, + "SickGearCommand": "sg.history", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearHistory.__init__(self, handler, args, kwargs) + + +class CMD_SickGearHistoryClear(ApiCall): + _help = {"desc": "clear sickgear's history", + } def __init__(self, handler, args, kwargs): # required @@ -1196,16 +1588,29 @@ class CMD_HistoryClear(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ clear sickbeard's history """ + """ clear sickgear's history """ myDB = db.DBConnection() myDB.action("DELETE FROM history WHERE 1=1") return _responds(RESULT_SUCCESS, msg="History cleared") -class CMD_HistoryTrim(ApiCall): - _help = {"desc": "trim sickbeard's history by removing entries greater than 30 days old" - } +class CMD_HistoryClear(CMD_SickGearHistoryClear): + _help = {"desc": "clear sickgear's history", + "SickGearCommand": "sg.history.clear", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearHistoryClear.__init__(self, handler, args, kwargs) + + +class CMD_SickGearHistoryTrim(ApiCall): + _help = {"desc": "trim sickgear's history by removing entries greater than 30 days old" + } def __init__(self, handler, args, kwargs): # required @@ -1214,7 +1619,7 @@ class CMD_HistoryTrim(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ trim sickbeard's history """ + """ trim sickgear's history """ myDB = db.DBConnection() myDB.action("DELETE FROM history WHERE date < " + str( (datetime.datetime.today() - datetime.timedelta(days=30)).strftime(history.dateFormat))) @@ -1222,11 +1627,24 @@ class CMD_HistoryTrim(ApiCall): return _responds(RESULT_SUCCESS, msg="Removed history entries greater than 30 days old") -class CMD_Logs(ApiCall): - _help = {"desc": "view sickbeard's log", +class CMD_HistoryTrim(CMD_SickGearHistoryTrim): + _help = {"desc": "trim sickgear's history by removing entries greater than 30 days old", + "SickGearCommand": "sg.history.trim", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearHistoryTrim.__init__(self, handler, args, kwargs) + + +class CMD_SickGearLogs(ApiCall): + _help = {"desc": "view sickgear's log", "optionalParameters": {"min_level ": { "desc": "the minimum level classification of log entries to show, with each level inherting its above level"}} - } + } def __init__(self, handler, args, kwargs): # required @@ -1297,7 +1715,22 @@ class CMD_Logs(ApiCall): return _responds(RESULT_SUCCESS, final_data) -class CMD_PostProcess(ApiCall): +class CMD_Logs(CMD_SickGearLogs): + _help = {"desc": "view sickgear's log", + "optionalParameters": {"min_level ": { + "desc": "the minimum level classification of log entries to show, with each level inherting its above level"}}, + "SickGearCommand": "sg.logs", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearLogs.__init__(self, handler, args, kwargs) + + +class CMD_SickGearPostProcess(ApiCall): _help = {"desc": "Manual postprocess TV Download Dir", "optionalParameters": {"path": {"desc": "Post process this folder"}, "force_replace": {"desc": "Force already Post Processed Dir/Files"}, @@ -1339,8 +1772,28 @@ class CMD_PostProcess(ApiCall): return _responds(RESULT_SUCCESS, data=data, msg="Started postprocess for %s" % self.path) -class CMD_SickBeard(ApiCall): - _help = {"desc": "display misc sickbeard related information"} +class CMD_PostProcess(CMD_SickGearPostProcess): + _help = {"desc": "Manual postprocess TV Download Dir", + "optionalParameters": {"path": {"desc": "Post process this folder"}, + "force_replace": {"desc": "Force already Post Processed Dir/Files"}, + "return_data": {"desc": "Returns result for the process"}, + "process_method": {"desc": "Symlink, hardlink, move or copy the file"}, + "is_priority": {"desc": "Replace the file even if it exists in a higher quality)"}, + "type": {"desc": "What type of postprocess request is this, auto of manual"} + }, + "SickGearCommand": "sg.postprocess", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearPostProcess.__init__(self, handler, args, kwargs) + + +class CMD_SickGear(ApiCall): + _help = {"desc": "display misc sickgear related information"} def __init__(self, handler, args, kwargs): # required @@ -1349,19 +1802,31 @@ class CMD_SickBeard(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ display misc sickbeard related information """ - data = {"sb_version": sickbeard.BRANCH, "api_version": Api.version, - "api_commands": sorted(_functionMaper.keys())} + """ display misc sickgear related information """ + data = {"sb_version": sickbeard.BRANCH, "api_version": Api.version, "fork": "SickGear", + "api_commands": sorted(x for x in _functionMaper.keys() if 'listcommands' != x)} return _responds(RESULT_SUCCESS, data) -class CMD_SickBeardAddRootDir(ApiCall): - _help = {"desc": "add a sickbeard user's parent directory", +class CMD_SickBeard(CMD_SickGear): + _help = {"desc": "display misc sickgear related information", + "SickGearCommand": "sg",} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGear.__init__(self, handler, args, kwargs) + + +class CMD_SickGearAddRootDir(ApiCall): + _help = {"desc": "add a sickgear user's parent directory", "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"} - }, + }, "optionalParameters": {"default": {"desc": "make the location passed the default root (parent) directory"} - } - } + } + } def __init__(self, handler, args, kwargs): # required @@ -1372,7 +1837,7 @@ class CMD_SickBeardAddRootDir(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ add a parent directory to sickbeard's config """ + """ add a parent directory to sickgear's config """ self.location = urllib.unquote_plus(self.location) location_matched = 0 @@ -1413,7 +1878,24 @@ class CMD_SickBeardAddRootDir(ApiCall): return _responds(RESULT_SUCCESS, _getRootDirs(), msg="Root directories updated") -class CMD_SickBeardCheckScheduler(ApiCall): +class CMD_SickBeardAddRootDir(CMD_SickGearAddRootDir): + _help = {"desc": "add a sickgear user's parent directory", + "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"} + }, + "optionalParameters": {"default": {"desc": "make the location passed the default root (parent) directory"} + }, + "SickGearCommand": "sg.addrootdir", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearAddRootDir.__init__(self, handler, args, kwargs) + + +class CMD_SickGearCheckScheduler(ApiCall): _help = {"desc": "query the scheduler"} def __init__(self, handler, args, kwargs): @@ -1437,10 +1919,22 @@ class CMD_SickBeardCheckScheduler(ApiCall): return _responds(RESULT_SUCCESS, data) -class CMD_SickBeardDeleteRootDir(ApiCall): - _help = {"desc": "delete a sickbeard user's parent directory", +class CMD_SickBeardCheckScheduler(CMD_SickGearCheckScheduler): + _help = {"desc": "query the scheduler", + "SickGearCommand": "sg.checkscheduler"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearCheckScheduler.__init__(self, handler, args, kwargs) + + +class CMD_SickGearDeleteRootDir(ApiCall): + _help = {"desc": "delete a sickgear user's parent directory", "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}} - } + } def __init__(self, handler, args, kwargs): # required @@ -1450,7 +1944,7 @@ class CMD_SickBeardDeleteRootDir(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ delete a parent directory from sickbeard's config """ + """ delete a parent directory from sickgear's config """ if sickbeard.ROOT_DIRS == "": return _responds(RESULT_FAILURE, _getRootDirs(), msg="No root directories detected") @@ -1483,26 +1977,63 @@ class CMD_SickBeardDeleteRootDir(ApiCall): return _responds(RESULT_SUCCESS, _getRootDirs(), msg="Root directory deleted") -class CMD_SickBeardForceSearch(ApiCall): - _help = {'desc': 'force the episode recent search early'} +class CMD_SickBeardDeleteRootDir(CMD_SickGearDeleteRootDir): + _help = {"desc": "delete a sickgear user's parent directory", + "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}}, + "SickGearCommand": "sg.deleterootdir" + } def __init__(self, handler, args, kwargs): # required # optional # super, missing, help + self.sickbeard_call = True + CMD_SickGearDeleteRootDir.__init__(self, handler, args, kwargs) + + +class CMD_SickGearForceSearch(ApiCall): + _help = {'desc': 'force the given search type searches', + "requiredParameters": {"searchtype": {"desc": "type of search to be forced: recent, backlog, proper"}} + } + + def __init__(self, handler, args, kwargs): + # required + self.searchtype, args = self.check_params(args, kwargs, "searchtype", "recent", True, "string", + ["recent", "backlog", "proper"]) + # optional + # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ force the episode search early """ - # Searching all providers for any needed episodes - result = sickbeard.recentSearchScheduler.forceRun() + """ force the given search type search """ + result = None + if 'recent' == self.searchtype: + result = sickbeard.recentSearchScheduler.forceRun() + elif 'backlog' == self.searchtype: + result = sickbeard.backlogSearchScheduler.force_search(force_type=FORCED_BACKLOG) + elif 'proper' == self.searchtype: + result = sickbeard.properFinderScheduler.forceRun() if result: - return _responds(RESULT_SUCCESS, msg='Episode recent search successfully forced') - return _responds(RESULT_FAILURE, msg='Can not force the episode recent search because it\'s already active') + return _responds(RESULT_SUCCESS, msg='%s search successfully forced' % self.searchtype) + return _responds(RESULT_FAILURE, + msg='Can not force the %s search because it\'s already active' % self.searchtype) -class CMD_SickBeardGetDefaults(ApiCall): - _help = {"desc": "get sickbeard user defaults"} +class CMD_SickBeardForceSearch(CMD_SickGearForceSearch): + _help = {'desc': 'force the episode recent search early', + "SickGearCommand": "sg.forcesearch",} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + kwargs['searchtype'] = 'recent' + self.sickbeard_call = True + CMD_SickGearForceSearch.__init__(self, handler, args, kwargs) + + +class CMD_SickGearSearchQueue(ApiCall): + _help = {'desc': 'list sickgear\'s search queue'} def __init__(self, handler, args, kwargs): # required @@ -1511,17 +2042,43 @@ class CMD_SickBeardGetDefaults(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ get sickbeard user defaults """ + """ list sickgear's search queue """ + return _responds(RESULT_SUCCESS, sickbeard.searchQueueScheduler.action.queue_length()) + + +class CMD_SickGearGetDefaults(ApiCall): + _help = {"desc": "get sickgear user defaults"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ get sickgear user defaults """ anyQualities, bestQualities = _mapQuality(sickbeard.QUALITY_DEFAULT) data = {"status": statusStrings[sickbeard.STATUS_DEFAULT].lower(), "flatten_folders": int(sickbeard.FLATTEN_FOLDERS_DEFAULT), "initial": anyQualities, - "archive": bestQualities, "future_show_paused": int(sickbeard.EPISODE_VIEW_DISPLAY_PAUSED)} + "archive": bestQualities, "future_show_paused": int(sickgear.EPISODE_VIEW_DISPLAY_PAUSED)} return _responds(RESULT_SUCCESS, data) -class CMD_SickBeardGetMessages(ApiCall): +class CMD_SickBeardGetDefaults(CMD_SickGearGetDefaults): + _help = {"desc": "get sickgear user defaults", + "SickGearCommand": "sg.getdefaults"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearGetDefaults.__init__(self, handler, args, kwargs) + + +class CMD_SickGearGetMessages(ApiCall): _help = {"desc": "get all messages"} def __init__(self, handler, args, kwargs): @@ -1539,8 +2096,20 @@ class CMD_SickBeardGetMessages(ApiCall): return _responds(RESULT_SUCCESS, messages) -class CMD_SickBeardGetRootDirs(ApiCall): - _help = {"desc": "get sickbeard user parent directories"} +class CMD_SickBeardGetMessages(CMD_SickGearGetMessages): + _help = {"desc": "get all messages", + "SickGearCommand": "sg.getmessages"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearGetMessages.__init__(self, handler, args, kwargs) + + +class CMD_SickGearGetQualities(ApiCall): + _help = {"desc": "get all qualities"} def __init__(self, handler, args, kwargs): # required @@ -1549,15 +2118,135 @@ class CMD_SickBeardGetRootDirs(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ get the parent directories defined in sickbeard's config """ + return _responds(RESULT_SUCCESS, quality_map) + + +class CMD_SickGearGetIndexers(ApiCall): + _help = {"desc": "get indexer list", + "optionalParameters": {"searchable-only ": {"desc": "searchable indexers only"}}} + + def __init__(self, handler, args, kwargs): + # required + # optional + self.searchable_only, args = self.check_params(args, kwargs, "searchable-only", False, False, "bool", []) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + result = {} + for i in indexer_config.indexerConfig: + for d, v in indexer_config.indexerConfig[i].iteritems(): + if self.searchable_only and (indexer_config.indexerConfig[i].get('mapped_only') or + not indexer_config.indexerConfig[i].get('active') or + indexer_config.indexerConfig[i].get('defunct')): + continue + if d in ['id', 'name', 'show_url', 'mapped_only', 'main_url'] and \ + isinstance(v, (basestring, tuple, dict, list, int, long, float, bool)): + if 'mapped_only' == d: + key = 'searchable' + val = not v and indexer_config.indexerConfig[i].get('active') and \ + not indexer_config.indexerConfig[i].get('defunct') + else: + key = d + val = (v, '%s{INDEXER-ID}' % v)['show_url' == d] + result.setdefault(i, {}).update({key: val}) + return _responds(RESULT_SUCCESS, result) + + +class CMD_SickGearGetIndexerIcon(ApiCall): + _help = {"desc": "get indexer icon", + "requiredParameters": {"indexer": {"desc": "indexer"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().all_indexers]) + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + # doesn't work + i = indexer_config.indexerConfig.get(self.indexer) + if not i: + self.handler.set_status(404) + return _responds(RESULT_FAILURE, 'Icon not found') + img = i['icon'] + image = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', 'images', img) + if not ek.ek(os.path.isfile, image): + self.handler.set_status(404) + return _responds(RESULT_FAILURE, 'Icon not found') + return {'outputType': 'image', 'image': self.handler.getImage(image)} + + +class CMD_SickGearGetNetworkIcon(ApiCall): + _help = {"desc": "get network icon", + "requiredParameters": {"network": {"desc": "name of network"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.network, args = self.check_params(args, kwargs, "network", None, True, "string", []) + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + image = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', 'images', 'network', + '%s.png' % self.network.lower()) + if not ek.ek(os.path.isfile, image): + self.handler.set_status(404) + return _responds(RESULT_FAILURE, 'Icon not found') + return {'outputType': 'image', 'image': self.handler.getImage(image)} + + +class CMD_SickGearGetqualityStrings(ApiCall): + _help = {"desc": "get human readable quality strings"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + return _responds(RESULT_SUCCESS, Quality.qualityStrings) + + +class CMD_SickGearGetRootDirs(ApiCall): + _help = {"desc": "get sickgear user parent directories"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ get the parent directories defined in sickgear's config """ return _responds(RESULT_SUCCESS, _getRootDirs()) -class CMD_SickBeardPauseBacklog(ApiCall): +class CMD_SickBeardGetRootDirs(CMD_SickGearGetRootDirs): + _help = {"desc": "get sickgear user parent directories", + "SickGearCommand": "sg.getrootdirs"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearGetRootDirs.__init__(self, handler, args, kwargs) + + +class CMD_SickGearPauseBacklog(ApiCall): _help = {"desc": "pause the backlog search", "optionalParameters": {"pause ": {"desc": "pause or unpause the global backlog"}} - } + } def __init__(self, handler, args, kwargs): # required @@ -1576,8 +2265,22 @@ class CMD_SickBeardPauseBacklog(ApiCall): return _responds(RESULT_SUCCESS, msg="Backlog unpaused") -class CMD_SickBeardPing(ApiCall): - _help = {"desc": "check to see if sickbeard is running"} +class CMD_SickBeardPauseBacklog(CMD_SickGearPauseBacklog): + _help = {"desc": "pause the backlog search", + "optionalParameters": {"pause ": {"desc": "pause or unpause the global backlog"}}, + "SickGearCommand": "sg.pausebacklog" + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearPauseBacklog.__init__(self, handler, args, kwargs) + + +class CMD_SickGearPing(ApiCall): + _help = {"desc": "check to see if sickgear is running",} def __init__(self, handler, args, kwargs): # required @@ -1586,7 +2289,7 @@ class CMD_SickBeardPing(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ check to see if sickbeard is running """ + """ check to see if sickgear is running """ self.handler.set_header('Cache-Control', "max-age=0,no-cache,no-store") if sickbeard.started: return _responds(RESULT_SUCCESS, {"pid": sickbeard.PID}, "Pong") @@ -1594,8 +2297,20 @@ class CMD_SickBeardPing(ApiCall): return _responds(RESULT_SUCCESS, msg="Pong") -class CMD_SickBeardRestart(ApiCall): - _help = {"desc": "restart sickbeard"} +class CMD_SickBeardPing(CMD_SickGearPing): + _help = {"desc": "check to see if sickgear is running", + "SickGearCommand": "sg.ping"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearPing.__init__(self, handler, args, kwargs) + + +class CMD_SickGearRestart(ApiCall): + _help = {"desc": "restart sickgear"} def __init__(self, handler, args, kwargs): # required @@ -1604,16 +2319,29 @@ class CMD_SickBeardRestart(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ restart sickbeard """ + """ restart sickgear """ sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) return _responds(RESULT_SUCCESS, msg="SickGear is restarting...") -class CMD_SickBeardSearchIndexers(ApiCall): +class CMD_SickBeardRestart(CMD_SickGearRestart): + _help = {"desc": "restart sickgear", + "SickGearCommand": "sg.restart"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearRestart.__init__(self, handler, args, kwargs) + + +class CMD_SickGearSearchIndexers(ApiCall): _help = {"desc": "search for show on the indexers with a given string and language", "optionalParameters": {"name": {"desc": "name of the show you want to search for"}, "indexerid": {"desc": "thetvdb.com or tvrage.com unique id of a show"}, - "lang": {"desc": "the 2 letter abbreviation lang id"} + "lang": {"desc": "the 2 letter abbreviation lang id"}, + "indexer": {"desc": "indexer to search, use -1 to search all indexers"} } } @@ -1628,54 +2356,74 @@ class CMD_SickBeardSearchIndexers(ApiCall): # optional self.name, args = self.check_params(args, kwargs, "name", None, False, "string", []) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", []) - self.lang, args = self.check_params(args, kwargs, "lang", "en", False, "string", self.valid_languages.keys()) - self.indexer, args = self.check_params(args, kwargs, "indexer", 1, False, "int", []) + # self.lang, args = self.check_params(args, kwargs, "lang", "en", False, "string", self.valid_languages.keys()) + self.indexers, args = self.check_params(args, kwargs, "indexers", -1, False, "list", + [-1] + [i for i in indexer_api.indexerApi().search_indexers], int) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ search for show at tvdb with a given string and language """ + """ search for show at indexers with a given string and language """ + if 1 > len(self.indexers) and -1 in self.indexers: + raise ApiError('Mix of -1 (all Indexer) and specific Indexer not allowed') + + all_indexer = 1 == len(self.indexers) and -1 == self.indexers[0] + if self.name and not self.indexerid: # only name was given - lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy() - lINDEXER_API_PARMS['language'] = self.lang - lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsListUI - t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS) - - apiData = None - - try: - apiData = t[str(self.name).encode()] - except Exception as e: - pass - - if not apiData: - return _responds(RESULT_FAILURE, msg="Did not get result from tvdb") - results = [] - for curSeries in apiData: - results.append({"indexerid": int(curSeries['id']), - "tvdbid": int(curSeries['id']), - "name": curSeries['seriesname'], - "first_aired": curSeries['firstaired'], - "indexer": self.indexer}) + indexertosearch = (self.indexers, [i for i in indexer_api.indexerApi().indexers if + indexer_api.indexerApi(i).config.get('active') and + not indexer_api.indexerApi(i).config.get('mapped_only') and + not indexer_api.indexerApi(i).config.get('defunct')])[all_indexer] + for i in indexertosearch: + lINDEXER_API_PARMS = sickbeard.indexerApi(i).api_params.copy() + lINDEXER_API_PARMS['language'] = 'en' + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI + t = sickbeard.indexerApi(i).indexer(**lINDEXER_API_PARMS) - lang_id = self.valid_languages[self.lang] - return _responds(RESULT_SUCCESS, {"results": results, "langid": lang_id}) + apiData = None - elif self.indexerid: - lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy() + try: + apiData = t[str(self.name).encode(), False] + except (StandardError, Exception): + pass - lang_id = self.valid_languages[self.lang] - if self.lang and not self.lang == 'en': - lINDEXER_API_PARMS['language'] = self.lang + for curSeries in apiData: + s = {"indexerid": int(curSeries['id']), + "name": curSeries['seriesname'], + "first_aired": curSeries['firstaired'], + "indexer": i, + "aliases": curSeries.get('aliases', None), + "relevance": NewHomeAddShows.get_UWRatio(self.name, curSeries['seriesname'], + curSeries.get('aliases', None))} + if INDEXER_TVDB == i: + s["tvdbid"] = int(curSeries['id']) + else: + s["tvdbid"] = None + results.append(s) + + if not results: + return _responds(RESULT_FAILURE, msg="Did not get result from %s" % + ', '.join([sickbeard.indexerApi(i).name for i in indexertosearch])) + + results = sorted(results, key=lambda x: x['relevance'], reverse=True) + + return _responds(RESULT_SUCCESS, {"results": results, "langid": 'en'}) + + elif self.indexerid and not all_indexer and 1 == len(self.indexers): + lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexers[0]).api_params.copy() + + lang_id = 'en' + lINDEXER_API_PARMS['language'] = 'en' + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI lINDEXER_API_PARMS['actors'] = False - t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS) + t = sickbeard.indexerApi(self.indexers[0]).indexer(**lINDEXER_API_PARMS) try: - myShow = t[int(self.indexerid)] + myShow = t[int(self.indexerid), False] except (sickbeard.indexer_shownotfound, sickbeard.indexer_error): logger.log(u"API :: Unable to find show with id " + str(self.indexerid), logger.WARNING) return _responds(RESULT_SUCCESS, {"results": [], "langid": lang_id}) @@ -1687,17 +2435,42 @@ class CMD_SickBeardSearchIndexers(ApiCall): return _responds(RESULT_FAILURE, msg="Show contains no name, invalid result") showOut = [{"indexerid": self.indexerid, - "tvdbid": self.indexerid, + "indexer": self.indexers[0], "name": unicode(myShow.data['seriesname']), - "first_aired": myShow.data['firstaired']}] + "first_aired": myShow.data['firstaired'], + "aliases": myShow.data.get('aliases', None), + "relevance": NewHomeAddShows.get_UWRatio(self.name, myShow.data['seriesname'], + myShow.data.get('aliases', None))}] + if INDEXER_TVDB == self.indexers[0]: + showOut[0]["tvdbid"] = int(myShow.data['id']) + else: + showOut[0]["tvdbid"] = None + + showOut = sorted(showOut, key=lambda x: x['relevance'], reverse=True) return _responds(RESULT_SUCCESS, {"results": showOut, "langid": lang_id}) else: - return _responds(RESULT_FAILURE, msg="Either indexerid or name is required") + return _responds(RESULT_FAILURE, msg="Either indexer + indexerid or name is required") + + +class CMD_SickBeardSearchIndexers(CMD_SickGearSearchIndexers): + _help = {"desc": "search for show on the tvdb with a given string and language", + "optionalParameters": {"name": {"desc": "name of the show you want to search for"}, + "indexerid": {"desc": "thetvdb.com unique id of a show"}, + "lang": {"desc": "the 2 letter abbreviation lang id"}, + }, + "SickGearCommand": "sg.searchtv", + } + + def __init__(self, handler, args, kwargs): + kwargs['indexers'] = INDEXER_TVDB + # super, missing, help + self.sickbeard_call = True + CMD_SickGearSearchIndexers.__init__(self, handler, args, kwargs) class CMD_SickBeardSetDefaults(ApiCall): - _help = {"desc": "set sickbeard user defaults", + _help = {"desc": "set sickgear user defaults", "optionalParameters": {"initial": {"desc": "initial quality for the show"}, "archive": {"desc": "archive quality for the show"}, "flatten_folders": {"desc": "flatten subfolders within the show directory"}, @@ -1708,12 +2481,8 @@ class CMD_SickBeardSetDefaults(ApiCall): def __init__(self, handler, args, kwargs): # required # optional - self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", - ["sdtv", "sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", - "fullhdwebdl", "hdbluray", "fullhdbluray", "unknown"]) - self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", - ["sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", "fullhdwebdl", - "hdbluray", "fullhdbluray"]) + self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map]) + self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map]) self.future_show_paused, args = self.check_params(args, kwargs, "future_show_paused", None, False, "bool", []) self.flatten_folders, args = self.check_params(args, kwargs, "flatten_folders", None, False, "bool", []) self.status, args = self.check_params(args, kwargs, "status", None, False, "string", @@ -1722,18 +2491,7 @@ class CMD_SickBeardSetDefaults(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ set sickbeard user defaults """ - - quality_map = {'sdtv': Quality.SDTV, - 'sddvd': Quality.SDDVD, - 'hdtv': Quality.HDTV, - 'rawhdtv': Quality.RAWHDTV, - 'fullhdtv': Quality.FULLHDTV, - 'hdwebdl': Quality.HDWEBDL, - 'fullhdwebdl': Quality.FULLHDWEBDL, - 'hdbluray': Quality.HDBLURAY, - 'fullhdbluray': Quality.FULLHDBLURAY, - 'unknown': Quality.UNKNOWN} + """ set sickgear user defaults """ iqualityID = [] aqualityID = [] @@ -1771,8 +2529,81 @@ class CMD_SickBeardSetDefaults(ApiCall): return _responds(RESULT_SUCCESS, msg="Saved defaults") +class CMD_SickGearSetSceneNumber(ApiCall): + _help = {"desc": "set Scene Numbers", + "requiredParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + }, + "optionalParameters": {"forSeason": {"desc": "season number of a show"}, + "forEpisode": {"desc": "episode number of a show"}, + "forAbsolute": {"desc": "absolute episode number of a show"}, + "sceneSeason": {"desc": "scene season number of a show to set"}, + "sceneEpisode": {"desc": "scene episode number of a show to set"}, + "sceneAbsolute": {"desc": "scene absolute episode number of a show to set"}, + } + } + + def __init__(self, handler, args, kwargs): + # required + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) + # optional + self.forSeason, args = self.check_params(args, kwargs, "forSeason", None, False, "int", []) + self.forEpisode, args = self.check_params(args, kwargs, "forEpisode", None, False, "int", []) + self.forAbsolute, args = self.check_params(args, kwargs, "forAbsolute", None, False, "int", []) + self.sceneSeason, args = self.check_params(args, kwargs, "sceneSeason", None, False, "int", []) + self.sceneEpisode, args = self.check_params(args, kwargs, "sceneEpisode", None, False, "int", []) + self.sceneAbsolute, args = self.check_params(args, kwargs, "sceneAbsolute", None, False, "int", []) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ saving scene numbers """ + + result = set_scene_numbering_helper(self.indexerid, self.indexer, self.forSeason, self.forEpisode, + self.forAbsolute, self.sceneSeason, self.sceneEpisode, self.sceneEpisode) + + if not result['success']: + return _responds(RESULT_FAILURE, result) + + return _responds(RESULT_SUCCESS, result) + + +class CMD_SickGearActivateSceneNumber(ApiCall): + _help = {"desc": "De-/Activate Scene Numbers", + "requiredParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + "activate": {"desc": "de-/activate scene numbering"}}, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.activate, args = self.check_params(args, kwargs, "activate", None, True, "bool", []) + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ de-/activate scene numbers """ + + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) + if not showObj: + return _responds(RESULT_FAILURE, msg="Can't find show") + + showObj.scene = int(self.activate) + showObj.saveToDB() + + return _responds(RESULT_SUCCESS, data={'indexer': self.indexer, 'indexerid': self.indexerid, + 'show_name': showObj.name, 'scenenumbering': showObj.is_scene}, + msg="Scene Numbering %sactivated" % ('de', '')[self.activate]) + + class CMD_SickBeardShutdown(ApiCall): - _help = {"desc": "shutdown sickbeard"} + _help = {"desc": "shutdown sickgear"} def __init__(self, handler, args, kwargs): # required @@ -1781,27 +2612,273 @@ class CMD_SickBeardShutdown(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ shutdown sickbeard """ + """ shutdown sickgear """ sickbeard.events.put(sickbeard.events.SystemEvent.SHUTDOWN) return _responds(RESULT_SUCCESS, msg="SickGear is shutting down...") -class CMD_Show(ApiCall): - _help = {"desc": "display information for a given show", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - } - } +class CMD_SickGearListIgnoreWords(ApiCall): + _help = {"desc": "list ignore words", + "optionalParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + } + } def __init__(self, handler, args, kwargs): # required + # optional + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, False, "int", + [i for i in indexer_api.indexerApi().indexers]) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ list ignore words """ + if self.indexer and self.indexerid: + myDB = db.DBConnection() + sqlResults = myDB.select('SELECT show_name, rls_ignore_words FROM tv_shows WHERE indexer = ? AND ' + 'indexer_id = ?', [self.indexer, self.indexerid]) + if sqlResults: + ignore_words = sqlResults[0]['rls_ignore_words'] + return_data = {'type': 'show', 'indexer': self.indexer, 'indexerid': self.indexerid, + 'show name': sqlResults[0]['show_name']} + return_type = '%s:' % sqlResults[0]['show_name'] + else: + return _responds(RESULT_FAILURE, msg='Show not found.') + elif (None is self.indexer) != (None is self.indexerid): + return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.') + else: + ignore_words = sickbeard.IGNORE_WORDS + return_data = {'type': 'global'} + return_type = 'Global' + + return_data['use regex'] = ignore_words.startswith('regex:') + return_data['ignore words'] = [w.strip() for w in ignore_words.replace('regex:', '').split(',')] + return _responds(RESULT_SUCCESS, data=return_data, msg="%s ignore words" % return_type) + + +class CMD_SickGearSetIgnoreWords(ApiCall): + _help = {"desc": "set ignore words", + "optionalParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + "add": {"desc": "add words to list"}, + "remove": {"desc": "remove words from list"}, + "regex": {"desc": "interpret ALL (including existing) ignore words as regex"}, + } + } + + def __init__(self, handler, args, kwargs): + # required + # optional + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, False, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.add, args = self.check_params(args, kwargs, "add", None, False, "list", []) + self.remove, args = self.check_params(args, kwargs, "remove", None, False, "list", []) + self.regex, args = self.check_params(args, kwargs, "regex", None, False, "bool", []) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ set ignore words """ + if not self.add and not self.remove: + return _responds(RESULT_FAILURE, msg="No words to add/remove provided") + + def _create_ignore_words(): + use_regex = ignore_words.startswith('regex:') + ignore_list = [w.strip() for w in ignore_words.replace('regex:', '').split(',')] + + if None is not self.regex: + use_regex = self.regex + if self.add: + for a in self.add: + ignore_list.append(a) + if self.remove: + for r in self.remove: + try: + ignore_list.remove(r) + except ValueError: + pass + return use_regex, ignore_list, '%s%s' % (('', 'regex:')[use_regex], ', '.join(ignore_list)) + + if self.indexer and self.indexerid: + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) + if not showObj: + return _responds(RESULT_FAILURE, msg="Show not found") + + myDB = db.DBConnection() + sqlResults = myDB.select('SELECT show_name, rls_ignore_words FROM tv_shows WHERE indexer = ? AND ' + 'indexer_id = ?', [self.indexer, self.indexerid]) + + ignore_words = '' + if sqlResults: + ignore_words = sqlResults[0]['rls_ignore_words'] + + return_data = {'type': 'show', 'indexer': self.indexer, 'indexerid': self.indexerid, + 'show name': sqlResults[0]['show_name']} + return_type = '%s:' % sqlResults[0]['show_name'] + + use_regex, ignore_list, new_ignore_words = _create_ignore_words() + myDB.action('UPDATE tv_shows SET rls_ignore_words = ? WHERE indexer = ? AND indexer_id = ?', + [new_ignore_words, self.indexer, self.indexerid]) + elif (None is self.indexer) != (None is self.indexerid): + return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.') + else: + ignore_words = sickbeard.IGNORE_WORDS + use_regex, ignore_list, new_ignore_words = _create_ignore_words() + sickbeard.IGNORE_WORDS = new_ignore_words + sickbeard.save_config() + return_data = {'type': 'global'} + return_type = 'Global' + + return_data['use regex'] = use_regex + return_data['ignore words'] = ignore_list + return _responds(RESULT_SUCCESS, data=return_data, msg="%s set ignore words" % return_type) + + +class CMD_SickGearListRequireWords(ApiCall): + _help = {"desc": "list requried words", + "optionalParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + } + } + + def __init__(self, handler, args, kwargs): + # required + # optional + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, False, "int", + [i for i in indexer_api.indexerApi().indexers]) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ list required words """ + if self.indexer and self.indexerid: + myDB = db.DBConnection() + sqlResults = myDB.select('SELECT show_name, rls_require_words FROM tv_shows WHERE indexer = ? ' + 'AND indexer_id = ?', [self.indexer, self.indexerid]) + if sqlResults: + required_words = sqlResults[0]['rls_require_words'] + return_data = {'type': 'show', 'indexer': self.indexer, 'indexerid': self.indexerid, + 'show name': sqlResults[0]['show_name']} + return_type = '%s:' % sqlResults[0]['show_name'] + else: + return _responds(RESULT_FAILURE, msg='Show not found.') + elif (None is self.indexer) != (None is self.indexerid): + return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.') + else: + required_words = sickbeard.REQUIRE_WORDS + return_data = {'type': 'global'} + return_type = 'Global' + + return_data['use regex'] = required_words.startswith('regex:') + return_data['required words'] = [w.strip() for w in required_words.replace('regex:', '').split(',')] + return _responds(RESULT_SUCCESS, data=return_data, msg="%s required words" % return_type) + + +class CMD_SickGearSetRequrieWords(ApiCall): + _help = {"desc": "set required words", + "optionalParameters": {"indexerid": {"desc": "unique id of a show"}, + "indexer": {"desc": "indexer of a show"}, + "add": {"desc": "add words to list"}, + "remove": {"desc": "remove words from list"}, + "regex": {"desc": "interpret ALL (including existing) ignore words as regex"}, + } + } + + def __init__(self, handler, args, kwargs): + # required + # optional + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, False, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.add, args = self.check_params(args, kwargs, "add", None, False, "list", []) + self.remove, args = self.check_params(args, kwargs, "remove", None, False, "list", []) + self.regex, args = self.check_params(args, kwargs, "regex", None, False, "bool", []) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ set ignore words """ + if not self.add and not self.remove: + return _responds(RESULT_FAILURE, msg="No words to add/remove provided") + + def _create_required_words(): + use_regex = requried_words.startswith('regex:') + require_list = [w.strip() for w in requried_words.replace('regex:', '').split(',')] + + if None is not self.regex: + use_regex = self.regex + if self.add: + for a in self.add: + require_list.append(a) + if self.remove: + for r in self.remove: + try: + require_list.remove(r) + except ValueError: + pass + return use_regex, require_list, '%s%s' % (('', 'regex:')[use_regex], ', '.join(require_list)) + + if self.indexer and self.indexerid: + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) + if not showObj: + return _responds(RESULT_FAILURE, msg="Show not found") + + myDB = db.DBConnection() + sqlResults = myDB.select('SELECT show_name, rls_require_words FROM tv_shows WHERE indexer = ? AND ' + 'indexer_id = ?', [self.indexer, self.indexerid]) + + requried_words = '' + if sqlResults: + requried_words = sqlResults[0]['rls_require_words'] + + return_data = {'type': 'show', 'indexer': self.indexer, 'indexerid': self.indexerid, + 'show name': sqlResults[0]['show_name']} + return_type = '%s:' % sqlResults[0]['show_name'] + + use_regex, required_list, new_required_words = _create_required_words() + myDB.action('UPDATE tv_shows SET rls_require_words = ? WHERE indexer = ? AND indexer_id = ?', + [new_required_words, self.indexer, self.indexerid]) + elif (None is self.indexer) != (None is self.indexerid): + return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.') + else: + requried_words = sickbeard.REQUIRE_WORDS + use_regex, required_list, new_required_words = _create_required_words() + sickbeard.REQUIRE_WORDS = new_required_words + sickbeard.save_config() + return_data = {'type': 'global'} + return_type = 'Global' + + return_data['use regex'] = use_regex + return_data['required words'] = required_list + return _responds(RESULT_SUCCESS, data=return_data, msg="%s set requried words" % return_type) + + +class CMD_SickGearShow(ApiCall): + _help = {"desc": "display information for a given show", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + "optionalParameters": {"overview": {"desc": "include overview"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional + self.overview, args = self.check_params(args, kwargs, "overview", False, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): """ display information for a given show """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -1838,15 +2915,31 @@ class CMD_Show(ApiCall): showDict["airs"] = str(showObj.airs).replace('am', ' AM').replace('pm', ' PM').replace(' ', ' ') showDict["indexerid"] = self.indexerid showDict["tvrage_id"] = showObj.ids.get(INDEXER_TVRAGE, {'id': 0})['id'] + showDict['ids'] = {k: v.get('id') for k, v in showObj.ids.iteritems()} showDict["tvrage_name"] = showObj.name showDict["network"] = showObj.network if not showDict["network"]: showDict["network"] = "" showDict["status"] = showObj.status + showDict["scenenumbering"] = showObj.is_scene + showDict["archivefirstmatch"] = showObj.upgrade_once + showDict["irgnorewords"] = showObj.rls_ignore_words + showDict["requirewords"] = showObj.rls_require_words + if self.overview: + showDict["overview"] = showObj.overview + showDict["tag"] = showObj.tag + showDict["imdb_id"] = showObj.imdbid + showDict["classification"] = showObj.classification + showDict["runtime"] = showObj.runtime + showDict["startyear"] = showObj.startyear + showDict["indexer"] = showObj.indexer + timezone, showDict['timezone'] = network_timezones.get_network_timezone(showDict['network'], return_name=True) if showObj.nextaired: - dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(showObj.nextaired, showDict['airs'], showDict['network'])) - showDict['airs'] = sbdatetime.sbdatetime.sbftime(dtEpisodeAirs, t_preset=timeFormat).lstrip('0').replace(' 0', ' ') + dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting( + network_timezones.parse_date_time(showObj.nextaired, showDict['airs'], timezone)) + showDict['airs'] = sbdatetime.sbdatetime.sbftime(dtEpisodeAirs, + t_preset=timeFormat).lstrip('0').replace(' 0', ' ') showDict['next_ep_airdate'] = sbdatetime.sbdatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat) else: showDict['next_ep_airdate'] = '' @@ -1854,39 +2947,49 @@ class CMD_Show(ApiCall): return _responds(RESULT_SUCCESS, showDict) -class CMD_ShowAddExisting(ApiCall): - _help = {"desc": "add a show in sickbeard with an existing folder", - "requiredParameters": {"tvdbid or tvrageid": {"desc": "thetvdb.com or tvrage.com id"}, +class CMD_Show(CMD_SickGearShow): + _help = {"desc": "display information for a given thetvdb.com show", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "optionalParameters": {"overview": {"desc": "include overview"}, + }, + "SickGearCommand": "sg.show", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShow.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowAddExisting(ApiCall): + _help = {"desc": "add a show in SickGear with an existing folder", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "indexer id of a show"}, "location": {"desc": "full path to the existing folder for the show"} }, "optionalParameters": {"initial": {"desc": "initial quality for the show"}, "archive": {"desc": "archive quality for the show"}, + "upgrade_once": {"desc": "upgrade only once"}, "flatten_folders": {"desc": "flatten subfolders for the show"}, "subtitles": {"desc": "allow search episode subtitle"} } } def __init__(self, handler, args, kwargs): - if "tvdbid" in args or "tvdbid" in kwargs: - _INDEXER_INT = 1 - _INDEXER = "tvdbid" - elif "tvrageid" in args or "tvrageid" in kwargs: - _INDEXER_INT = 2 - _INDEXER = "tvrageid" - else: - _INDEXER_INT = None - _INDEXER = None # required - self.indexerid, args = self.check_params(args, kwargs, _INDEXER, None, True, "int", []) - self.indexer = _INDEXER_INT + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().search_indexers]) self.location, args = self.check_params(args, kwargs, "location", None, True, "string", []) # optional - self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", - ["sdtv", "sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", - "fullhdwebdl", "hdbluray", "fullhdbluray", "unknown"]) - self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", - ["sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", "fullhdwebdl", - "hdbluray", "fullhdbluray"]) + self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map]) + self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map]) + self.upgradeonce, args = self.check_params(args, kwargs, "upgrade_once", False, False, "bool", []) + self.flatten_folders, args = self.check_params(args, kwargs, "flatten_folders", str(sickbeard.FLATTEN_FOLDERS_DEFAULT), False, "bool", []) self.subtitles, args = self.check_params(args, kwargs, "subtitles", int(sickbeard.USE_SUBTITLES), False, "int", @@ -1895,8 +2998,8 @@ class CMD_ShowAddExisting(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ add a show in sickbeard with an existing folder """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ add a show in sickgear with an existing folder """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if showObj: return _responds(RESULT_FAILURE, msg="An existing indexerid already exists in the database") @@ -1916,17 +3019,6 @@ class CMD_ShowAddExisting(ApiCall): if not indexerName: return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") - quality_map = {'sdtv': Quality.SDTV, - 'sddvd': Quality.SDDVD, - 'hdtv': Quality.HDTV, - 'rawhdtv': Quality.RAWHDTV, - 'fullhdtv': Quality.FULLHDTV, - 'hdwebdl': Quality.HDWEBDL, - 'fullhdwebdl': Quality.FULLHDWEBDL, - 'hdbluray': Quality.HDBLURAY, - 'fullhdbluray': Quality.FULLHDBLURAY, - 'unknown': Quality.UNKNOWN} - #use default quality as a failsafe newQuality = int(sickbeard.QUALITY_DEFAULT) iqualityID = [] @@ -1943,72 +3035,79 @@ class CMD_ShowAddExisting(ApiCall): newQuality = Quality.combineQualities(iqualityID, aqualityID) sickbeard.showQueueScheduler.action.addShow(int(self.indexer), int(self.indexerid), self.location, SKIPPED, - newQuality, int(self.flatten_folders)) + newQuality, int(self.flatten_folders), + upgrade_once=self.upgradeonce) return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added") -class CMD_ShowAddNew(ApiCall): - _help = {"desc": "add a new show to sickbeard", - "requiredParameters": {"tvdbid or tvrageid": {"desc": "thetvdb.com or tvrage.com id"} - }, +class CMD_ShowAddExisting(CMD_SickGearShowAddExisting): + _help = {"desc": "add a show in sickgear with an existing folder", + "requiredParameters": {"tvdbid": {"desc": "thetvdb.com id"}, + "location": {"desc": "full path to the existing folder for the show"} + }, + "optionalParameters": {"initial": {"desc": "initial quality for the show"}, + "archive": {"desc": "archive quality for the show"}, + "flatten_folders": {"desc": "flatten subfolders for the show"}, + "subtitles": {"desc": "allow search episode subtitle"} + }, + "SickGearCommand": "sg.show.addexisting", + } + + def __init__(self, handler, args, kwargs): + kwargs['indexer'] = INDEXER_TVDB + # required + kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", []) + # super, missing, help + self.sickbeard_call = True + CMD_SickGearShowAddExisting.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowAddNew(ApiCall): + _help = {"desc": "add a new show to sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of show"}, + "indexerid": {"desc": "id of show"}, + }, "optionalParameters": {"initial": {"desc": "initial quality for the show"}, "location": {"desc": "base path for where the show folder is to be created"}, "archive": {"desc": "archive quality for the show"}, + "upgrade_once": {"desc": "upgrade only once"}, "flatten_folders": {"desc": "flatten subfolders for the show"}, "status": {"desc": "status of missing episodes"}, - "lang": {"desc": "the 2 letter lang abbreviation id"}, "subtitles": {"desc": "allow search episode subtitle"}, "anime": {"desc": "set show to anime"}, "scene": {"desc": "show searches episodes by scene numbering"} - } - } - - valid_languages = { - 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, - 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, - 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, - 'sv': 8, 'sl': 30} + } + } def __init__(self, handler, args, kwargs): - if "tvdbid" in args or "tvdbid" in kwargs: - _INDEXER_INT = 1 - _INDEXER = "tvdbid" - elif "tvrageid" in args or "tvrageid" in kwargs: - _INDEXER_INT = 2 - _INDEXER = "tvrageid" - else: - _INDEXER_INT = None - _INDEXER = None # required - self.indexerid, args = self.check_params(args, kwargs, _INDEXER, None, True, "int", []) - self.indexer = _INDEXER_INT + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().search_indexers]) # optional self.location, args = self.check_params(args, kwargs, "location", None, False, "string", []) - self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", - ["sdtv", "sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", - "fullhdwebdl", "hdbluray", "fullhdbluray", "unknown"]) - self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", - ["sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", "fullhdwebdl", - "hdbluray", "fullhdbluray"]) + self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map]) + self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map]) + self.upgradeonce, args = self.check_params(args, kwargs, "upgrade_once", False, False, "bool", []) self.flatten_folders, args = self.check_params(args, kwargs, "flatten_folders", str(sickbeard.FLATTEN_FOLDERS_DEFAULT), False, "bool", []) self.status, args = self.check_params(args, kwargs, "status", None, False, "string", ["wanted", "skipped", "archived", "ignored"]) - self.lang, args = self.check_params(args, kwargs, "lang", "en", False, "string", self.valid_languages.keys()) self.subtitles, args = self.check_params(args, kwargs, "subtitles", int(sickbeard.USE_SUBTITLES), False, "int", []) self.anime, args = self.check_params(args, kwargs, "anime", int(sickbeard.ANIME_DEFAULT), False, "int", []) self.scene, args = self.check_params(args, kwargs, "scene", int(sickbeard.SCENE_DEFAULT), False, "int", []) + self.lang = 'en' # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ add a show in sickbeard with an existing folder """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ add a show in sickgear with an existing folder """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if showObj: return _responds(RESULT_FAILURE, msg="An existing indexerid already exists in database") @@ -2024,17 +3123,6 @@ class CMD_ShowAddNew(ApiCall): if not ek.ek(os.path.isdir, self.location): return _responds(RESULT_FAILURE, msg="'" + self.location + "' is not a valid location") - quality_map = {'sdtv': Quality.SDTV, - 'sddvd': Quality.SDDVD, - 'hdtv': Quality.HDTV, - 'rawhdtv': Quality.RAWHDTV, - 'fullhdtv': Quality.FULLHDTV, - 'hdwebdl': Quality.HDWEBDL, - 'fullhdwebdl': Quality.FULLHDWEBDL, - 'hdbluray': Quality.HDBLURAY, - 'fullhdbluray': Quality.FULLHDBLURAY, - 'unknown': Quality.UNKNOWN} - # use default quality as a failsafe newQuality = int(sickbeard.QUALITY_DEFAULT) iqualityID = [] @@ -2097,27 +3185,63 @@ class CMD_ShowAddNew(ApiCall): sickbeard.showQueueScheduler.action.addShow(int(self.indexer), int(self.indexerid), showPath, newStatus, newQuality, int(self.flatten_folders), self.lang, self.subtitles, self.anime, - self.scene, new_show=True) # @UndefinedVariable + self.scene, new_show=True, upgrade_once=self.upgradeonce) # @UndefinedVariable return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added") -class CMD_ShowCache(ApiCall): - _help = {"desc": "check sickbeard's cache to see if the banner or poster image for a show is valid", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"} +class CMD_ShowAddNew(CMD_SickGearShowAddNew): + _help = {"desc": "add a new show to sickgear", + "requiredParameters": {"tvdbid": {"desc": "thetvdb.com id"} + }, + "optionalParameters": {"initial": {"desc": "initial quality for the show"}, + "location": {"desc": "base path for where the show folder is to be created"}, + "archive": {"desc": "archive quality for the show"}, + "flatten_folders": {"desc": "flatten subfolders for the show"}, + "status": {"desc": "status of missing episodes"}, + "lang": {"desc": "the 2 letter lang abbreviation id"}, + "subtitles": {"desc": "allow search episode subtitle"}, + "anime": {"desc": "set show to anime"}, + "scene": {"desc": "show searches episodes by scene numbering"} + }, + "SickGearCommand": "sg.show.addnew", + } + + valid_languages = { + 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, + 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, + 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, + 'sv': 8, 'sl': 30} + + def __init__(self, handler, args, kwargs): + kwargs['indexer'] = INDEXER_TVDB + kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", []) + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearShowAddNew.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowCache(ApiCall): + _help = {"desc": "check sickgear's cache to see if the banner or poster image for a show is valid", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, } - } def __init__(self, handler, args, kwargs): # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ check sickbeard's cache to see if the banner or poster image for a show is valid """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ check sickgear's cache to see if the banner or poster image for a show is valid """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -2137,22 +3261,41 @@ class CMD_ShowCache(ApiCall): return _responds(RESULT_SUCCESS, {"poster": has_poster, "banner": has_banner}) -class CMD_ShowDelete(ApiCall): - _help = {"desc": "delete a show in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, +class CMD_ShowCache(CMD_SickGearShowCache): + _help = {"desc": "check sickgear's cache to see if the banner or poster image for a show is valid", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.cache", } - } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowCache.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowDelete(ApiCall): + _help = {"desc": "delete a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ delete a show in sickbeard """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ delete a show in sickgear """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -2164,76 +3307,285 @@ class CMD_ShowDelete(ApiCall): return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has been deleted") -class CMD_ShowGetQuality(ApiCall): - _help = {"desc": "get quality setting for a show in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"} +class CMD_ShowDelete(CMD_SickGearShowDelete): + _help = {"desc": "delete a show in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.delete", } - } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowDelete.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowGetQuality(ApiCall): + _help = {"desc": "get quality setting for a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ get quality setting for a show in sickbeard """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ get quality setting for a show in sickgear """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") anyQualities, bestQualities = _mapQuality(showObj.quality) - return _responds(RESULT_SUCCESS, {"initial": anyQualities, "archive": bestQualities}) + data = {"initial": anyQualities, "archive": bestQualities} + + if not self.sickbeard_call: + data['upgrade_once'] = showObj.upgrade_once + + return _responds(RESULT_SUCCESS, data) -class CMD_ShowGetPoster(ApiCall): - _help = {"desc": "get the poster stored for a show in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"} +class CMD_ShowGetQuality(CMD_SickGearShowGetQuality): + _help = {"desc": "get quality setting for a show in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.getquality", } - } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowGetQuality.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowGetPoster(ApiCall): + _help = {"desc": "get the poster stored for a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ get the poster for a show in sickbeard """ + """ get the poster for a show in sickgear """ return {'outputType': 'image', 'image': self.handler.showPoster(self.indexerid, 'poster', True)} -class CMD_ShowGetBanner(ApiCall): - _help = {"desc": "get the banner stored for a show in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"} +class CMD_ShowGetPoster(CMD_SickGearShowGetPoster): + _help = {"desc": "get the poster stored for a show in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.getposter", } - } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowGetPoster.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowGetBanner(ApiCall): + _help = {"desc": "get the banner stored for a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ get the banner for a show in sickbeard """ + """ get the banner for a show in sickgear """ return {'outputType': 'image', 'image': self.handler.showPoster(self.indexerid, 'banner', True)} -class CMD_ShowPause(ApiCall): - _help = {"desc": "set a show's paused state in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - }, - "optionalParameters": {"pause": {"desc": "set the pause state of the show"} +class CMD_ShowGetBanner(CMD_SickGearShowGetBanner): + _help = {"desc": "get the banner stored for a show in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.getbanner", } - } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowGetBanner.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowListFanart(ApiCall): + _help = {"desc": "list the fanart's stored for a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ list the fanart's for a show in sickgear """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) + if not showObj: + return _responds(RESULT_FAILURE, msg="Show not found") + + fanart = [] + rating_names = {10: 'group', 20: 'favorite', 30: 'avoid'} + cache_obj = image_cache.ImageCache() + for img in ek.ek(glob.glob, cache_obj.fanart_path(showObj.indexerid).replace('fanart.jpg', '*')) or []: + match = re.search(r'\.(\d+(?:\.(\w*?(\d*)))?\.(?:\w{5,8}))\.fanart\.', img, re.I) + if match and match.group(1): + fanart += [(match.group(1), rating_names.get(sickbeard.FANART_RATINGS.get(str(self.indexerid), {}).get(match.group(1), ''), ''))] + + return _responds(RESULT_SUCCESS, fanart) + + +class CMD_SickGearShowRateFanart(ApiCall): + _help = {"desc": "rate the fanart's stored for a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + "fanartname": {"desc": "fanart name form sg.show.listfanart"}, + "rating": {"desc": "rate: unrate, group, favorite, avoid"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + self.fanartname, args = self.check_params(args, kwargs, "fanartname", None, True, "string", []) + self.rating, args = self.check_params(args, kwargs, "rating", None, True, "string", + ['unrate', 'group', 'favorite', 'avoid']) + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ rate the fanart's for a show in sickgear """ + cache_obj = image_cache.ImageCache() + fanartfile = cache_obj.fanart_path(self.indexerid).replace('fanart.jpg', '%s.fanart.jpg' % self.fanartname) + if not ek.ek(os.path.isfile, fanartfile): + return _responds(RESULT_FAILURE, msg='Unknown Fanart') + fan_ratings = {'unrate': 0, 'group': 10, 'favorite': 20, 'avoid': 30} + if 'unrate' == self.rating and str(self.indexerid) in sickbeard.FANART_RATINGS \ + and self.fanartname in sickbeard.FANART_RATINGS[str(self.indexerid)]: + del sickbeard.FANART_RATINGS[str(self.indexerid)][self.fanartname] + else: + sickbeard.FANART_RATINGS[str(self.indexerid)][self.fanartname] = fan_ratings[self.rating] + sickbeard.save_config() + return _responds(RESULT_SUCCESS, msg='Rated Fanart: %s = %s' % (self.fanartname, self.rating)) + + +class CMD_SickGearShowGetFanart(ApiCall): + _help = {"desc": "get the fanart stored for a show in sickgear. X-Fanartname response header resturns Fanart name or default for not found", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + "optionalParameters": {"fanartname": {"desc": "fanart name form sg.show.listfanart"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) + self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) + # optional + self.fanartname, args = self.check_params(args, kwargs, "fanartname", None, False, "string", []) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ get the fanart for a show in sickgear """ + cache_obj = image_cache.ImageCache() + default_fanartfile = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', 'images', 'trans.png') + fanartfile = default_fanartfile + used_fanart = 'default' + if self.fanartname: + fanartfile = cache_obj.fanart_path(self.indexerid).replace('fanart.jpg', '%s.fanart.jpg' % self.fanartname) + if not ek.ek(os.path.isfile, fanartfile): + fanartfile = default_fanartfile + used_fanart = self.fanartname + else: + fanart = [] + for img in ek.ek(glob.glob, cache_obj.fanart_path(self.indexerid).replace('fanart.jpg', '*')) or []: + if not ek.ek(os.path.isfile, img): + continue + match = re.search(r'\.(\d+(?:\.(\w*?(\d*)))?\.(?:\w{5,8}))\.fanart\.', img, re.I) + if match and match.group(1): + fanart += [(img, match.group(1), sickbeard.FANART_RATINGS.get(str(self.indexerid), {}).get(match.group(1), 0))] + if fanart: + fanartsorted = sorted([f for f in fanart if f[2] != 30], key=lambda x: x[2], reverse=True) + max_fa = max([f[2] for f in fanartsorted]) + fanartsorted = [f for f in fanartsorted if f[2] == max_fa] + if fanartsorted: + random_fanart = randint(0, len(fanartsorted) - 1) + fanartfile = fanartsorted[random_fanart][0] + used_fanart = fanartsorted[random_fanart][1] + + if fanartfile and ek.ek(os.path.isfile, fanartfile): + with ek.ek(open, fanartfile, 'rb') as f: + mime_type, encoding = MimeTypes().guess_type(fanartfile) + self.handler.set_header('X-Fanartname', used_fanart) + self.handler.set_header('Content-Type', mime_type) + return {'outputType': 'image', 'image': f.read()} + + # we should never get here + return _responds(RESULT_FAILURE, msg='No Fanart found') + + +class CMD_SickGearShowPause(ApiCall): + _help = {"desc": "set a show's paused state in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + "optionalParameters": {"pause": {"desc": "set the pause state of the show"} + } + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional self.pause, args = self.check_params(args, kwargs, "pause", 0, False, "bool", []) @@ -2241,8 +3593,8 @@ class CMD_ShowPause(ApiCall): ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ set a show's paused state in sickbeard """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ set a show's paused state in sickgear """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -2256,22 +3608,42 @@ class CMD_ShowPause(ApiCall): return _responds(RESULT_FAILURE, msg=str(showObj.name) + " was unable to be paused") -class CMD_ShowRefresh(ApiCall): - _help = {"desc": "refresh a show in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - } - } +class CMD_ShowPause(CMD_SickGearShowPause): + _help = {"desc": "set a show's paused state in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "optionalParameters": {"pause": {"desc": "set the pause state of the show"} + }, + "SickGearCommand": "sg.show.pause", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowPause.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowRefresh(ApiCall): + _help = {"desc": "refresh a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"},}, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ refresh a show in sickbeard """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ refresh a show in sickgear """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -2283,16 +3655,35 @@ class CMD_ShowRefresh(ApiCall): return _responds(RESULT_FAILURE, msg="Unable to refresh " + str(showObj.name)) -class CMD_ShowSeasonList(ApiCall): - _help = {"desc": "display the season list for a given show", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - }, - "optionalParameters": {"sort": {"desc": "change the sort order from descending to ascending"} - } - } +class CMD_ShowRefresh(CMD_SickGearShowRefresh): + _help = {"desc": "refresh a show in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.refresh", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowRefresh.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowSeasonList(ApiCall): + _help = {"desc": "display the season list for a given show", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + "optionalParameters": {"sort": {"desc": "change the sort order from descending to ascending"} + } + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional self.sort, args = self.check_params(args, kwargs, "sort", "desc", False, "string", @@ -2302,17 +3693,19 @@ class CMD_ShowSeasonList(ApiCall): def run(self): """ display the season list for a given show """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") myDB = db.DBConnection(row_type="dict") if self.sort == "asc": - sqlResults = myDB.select("SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season ASC", - [self.indexerid]) + sqlResults = myDB.select("SELECT DISTINCT season FROM tv_episodes WHERE showid = ? AND indexer = ? " + "ORDER BY season ASC", + [self.indexerid, self.indexer]) else: - sqlResults = myDB.select("SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season DESC", - [self.indexerid]) + sqlResults = myDB.select("SELECT DISTINCT season FROM tv_episodes WHERE showid = ? AND indexer = ? " + "ORDER BY season DESC", + [self.indexerid, self.indexer]) seasonList = [] # a list with all season numbers for row in sqlResults: seasonList.append(int(row["season"])) @@ -2320,16 +3713,37 @@ class CMD_ShowSeasonList(ApiCall): return _responds(RESULT_SUCCESS, seasonList) -class CMD_ShowSeasons(ApiCall): - _help = {"desc": "display a listing of episodes for all or a given season", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - }, - "optionalParameters": {"season": {"desc": "the season number"}, - } - } +class CMD_ShowSeasonList(CMD_SickGearShowSeasonList): + _help = {"desc": "display the season list for a given show", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "optionalParameters": {"sort": {"desc": "change the sort order from descending to ascending"} + }, + "SickGearCommand": "sg.show.seasonlist", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowSeasonList.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowSeasons(ApiCall): + _help = {"desc": "display a listing of episodes for all or a given season", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + "optionalParameters": {"season": {"desc": "the season number"}, + } + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional self.season, args = self.check_params(args, kwargs, "season", None, False, "int", []) @@ -2338,22 +3752,30 @@ class CMD_ShowSeasons(ApiCall): def run(self): """ display a listing of episodes for all or a given show """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") myDB = db.DBConnection(row_type="dict") if self.season == None: - sqlResults = myDB.select("SELECT name, episode, airdate, status, season FROM tv_episodes WHERE showid = ?", - [self.indexerid]) + sqlResults = myDB.select("SELECT name, description, absolute_number, scene_absolute_number, episode, " + "scene_episode, scene_season, airdate, status, season FROM tv_episodes " + "WHERE showid = ? AND indexer = ?", + [self.indexerid, self.indexer]) seasons = {} for row in sqlResults: status, quality = Quality.splitCompositeStatus(int(row["status"])) row["status"] = _get_status_Strings(status) row["quality"] = _get_quality_string(quality) - dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(row['airdate'],showObj.airs,showObj.network)) + timezone, row['timezone'] = network_timezones.get_network_timezone(showObj.network, return_name=True) + dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time( + row['airdate'], showObj.airs, timezone)) row['airdate'] = sbdatetime.sbdatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat) + row['scene_episode'] = helpers.tryInt(row['scene_episode']) + row['scene_season'] = helpers.tryInt(row['scene_season']) + row['absolute_number'] = helpers.tryInt(row['absolute_number']) + row['scene_absolute_number'] = helpers.tryInt(row['scene_absolute_number']) curSeason = int(row["season"]) curEpisode = int(row["episode"]) del row["season"] @@ -2364,8 +3786,9 @@ class CMD_ShowSeasons(ApiCall): else: sqlResults = myDB.select( - "SELECT name, episode, airdate, status FROM tv_episodes WHERE showid = ? AND season = ?", - [self.indexerid, self.season]) + "SELECT name, description, absolute_number, scene_absolute_number, episode, scene_episode, " + "scene_season, airdate, status FROM tv_episodes WHERE showid = ? AND indexer = ? AND season = ?", + [self.indexerid, self.indexer, self.season]) if len(sqlResults) is 0: return _responds(RESULT_FAILURE, msg="Season not found") seasons = {} @@ -2375,8 +3798,14 @@ class CMD_ShowSeasons(ApiCall): status, quality = Quality.splitCompositeStatus(int(row["status"])) row["status"] = _get_status_Strings(status) row["quality"] = _get_quality_string(quality) - dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(row['airdate'], showObj.airs, showObj.network)) + timezone, row['timezone'] = network_timezones.get_network_timezone(showObj.network, return_name=True) + dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time( + row['airdate'], showObj.airs, timezone)) row['airdate'] = sbdatetime.sbdatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat) + row['scene_episode'] = helpers.tryInt(row['scene_episode']) + row['scene_season'] = helpers.tryInt(row['scene_season']) + row['absolute_number'] = helpers.tryInt(row['absolute_number']) + row['scene_absolute_number'] = helpers.tryInt(row['scene_absolute_number']) if not curEpisode in seasons: seasons[curEpisode] = {} seasons[curEpisode] = row @@ -2384,50 +3813,58 @@ class CMD_ShowSeasons(ApiCall): return _responds(RESULT_SUCCESS, seasons) -class CMD_ShowSetQuality(ApiCall): - _help = { - "desc": "set desired quality of a show in sickbeard. if neither initial or archive are provided then the config default quality will be used", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"} - }, - "optionalParameters": {"initial": {"desc": "initial quality for the show"}, - "archive": {"desc": "archive quality for the show"} - } - } +class CMD_ShowSeasons(CMD_SickGearShowSeasons): + _help = {"desc": "display a listing of episodes for all or a given season", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "optionalParameters": {"season": {"desc": "the season number"}, + }, + "SickGearCommand": "sg.show.seasons", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowSeasons.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowSetQuality(ApiCall): + _help = { + "desc": "set desired quality of a show in sickgear. if neither initial or archive are provided then the config default quality will be used", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"} + }, + "optionalParameters": {"initial": {"desc": "initial quality for the show"}, + "archive": {"desc": "archive quality for the show"}, + "upgrade_once": {"desc": "upgrade only once"} + } + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # this for whatever reason removes hdbluray not sdtv... which is just wrong. reverting to previous code.. plus we didnt use the new code everywhere. # self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", _getQualityMap().values()[1:]) - self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", - ["sdtv", "sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", - "fullhdwebdl", "hdbluray", "fullhdbluray", "unknown"]) - self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", - ["sddvd", "hdtv", "rawhdtv", "fullhdtv", "hdwebdl", "fullhdwebdl", - "hdbluray", "fullhdbluray"]) + self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map]) + self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map]) + self.upgradeonce, args = self.check_params(args, kwargs, "upgrade_once", False, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ set the quality for a show in sickbeard by taking in a deliminated + """ set the quality for a show in sickgear by taking in a deliminated string of qualities, map to their value and combine for new values """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") - quality_map = {'sdtv': Quality.SDTV, - 'sddvd': Quality.SDDVD, - 'hdtv': Quality.HDTV, - 'rawhdtv': Quality.RAWHDTV, - 'fullhdtv': Quality.FULLHDTV, - 'hdwebdl': Quality.HDWEBDL, - 'fullhdwebdl': Quality.FULLHDWEBDL, - 'hdbluray': Quality.HDBLURAY, - 'fullhdbluray': Quality.FULLHDBLURAY, - 'unknown': Quality.UNKNOWN} - #use default quality as a failsafe newQuality = int(sickbeard.QUALITY_DEFAULT) iqualityID = [] @@ -2444,18 +3881,46 @@ class CMD_ShowSetQuality(ApiCall): newQuality = Quality.combineQualities(iqualityID, aqualityID) showObj.quality = newQuality + showObj.upgrade_once = self.upgradeonce + + showObj.saveToDB() + return _responds(RESULT_SUCCESS, msg=showObj.name + " quality has been changed to " + _get_quality_string(showObj.quality)) -class CMD_ShowStats(ApiCall): - _help = {"desc": "display episode statistics for a given show", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, - } - } +class CMD_ShowSetQuality(CMD_SickGearShowSetQuality): + _help = { + "desc": "set desired quality of a show in sickgear. if neither initial or archive are provided then the config default quality will be used", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"} + }, + "optionalParameters": {"initial": {"desc": "initial quality for the show"}, + "archive": {"desc": "archive quality for the show"} + }, + "SickGearCommand": "sg.show.setquality", + } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowSetQuality.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowStats(ApiCall): + _help = {"desc": "display episode statistics for a given show", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help @@ -2463,7 +3928,7 @@ class CMD_ShowStats(ApiCall): def run(self): """ display episode statistics for a given show """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -2494,8 +3959,9 @@ class CMD_ShowStats(ApiCall): episode_qualities_counts_snatch[statusCode] = 0 myDB = db.DBConnection(row_type="dict") - sqlResults = myDB.select("SELECT status, season FROM tv_episodes WHERE season != 0 AND showid = ?", - [self.indexerid]) + sqlResults = myDB.select("SELECT status, season FROM tv_episodes WHERE season != 0 AND showid = ? " + "AND indexer = ?", + [self.indexerid, self.indexer]) # the main loop that goes through all episodes for row in sqlResults: status, quality = Quality.splitCompositeStatus(int(row["status"])) @@ -2552,22 +4018,41 @@ class CMD_ShowStats(ApiCall): return _responds(RESULT_SUCCESS, episodes_stats) -class CMD_ShowUpdate(ApiCall): - _help = {"desc": "update a show in sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"}, +class CMD_ShowStats(CMD_SickGearShowStats): + _help = {"desc": "display episode statistics for a given show", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.stats", } - } def __init__(self, handler, args, kwargs): # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowStats.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowUpdate(ApiCall): + _help = {"desc": "update a show in sickgear", + "requiredParameters": {"indexer": {"desc": "indexer of a show"}, + "indexerid": {"desc": "unique id of a show"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.indexer, args = self.check_params(args, kwargs, "indexer", None, True, "int", + [i for i in indexer_api.indexerApi().search_indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ update a show in sickbeard """ - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.indexerid)) + """ update a show in sickgear """ + showObj = helpers.find_show_by_id(sickbeard.showList, {self.indexer: self.indexerid}) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -2579,27 +4064,49 @@ class CMD_ShowUpdate(ApiCall): return _responds(RESULT_FAILURE, msg="Unable to update " + str(showObj.name)) -class CMD_Shows(ApiCall): - _help = {"desc": "display all shows in sickbeard", +class CMD_ShowUpdate(CMD_SickGearShowUpdate): + _help = {"desc": "update a show in sickgear", + "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}, + }, + "SickGearCommand": "sg.show.update", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + kwargs['indexer'] = INDEXER_TVDB + self.sickbeard_call = True + CMD_SickGearShowUpdate.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShows(ApiCall): + _help = {"desc": "display all shows in sickgear", "optionalParameters": {"sort": {"desc": "sort the list of shows by show name instead of indexerid"}, "paused": {"desc": "only show the shows that are set to paused"}, - }, - } + "overview": {"desc": "include overview"}, + }, + } def __init__(self, handler, args, kwargs): # required # optional self.sort, args = self.check_params(args, kwargs, "sort", "id", False, "string", ["id", "name"]) self.paused, args = self.check_params(args, kwargs, "paused", None, False, "bool", []) + self.overview, args = self.check_params(args, kwargs, "overview", False, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): - """ display_is_int_multi( self.indexerid )shows in sickbeard """ + """ display_is_int_multi( self.indexerid )shows in sickgear """ shows = {} + for curShow in sickbeard.showList: - if self.paused != None and bool(self.paused) != bool(curShow.paused): + if self.sickbeard_call and INDEXER_TVDB != curShow.indexer: + continue + + if self.paused is not None and bool(self.paused) != bool(curShow.paused): continue showDict = { @@ -2610,17 +4117,35 @@ class CMD_Shows(ApiCall): "sports": curShow.sports, "anime": curShow.anime, "indexerid": curShow.indexerid, + "indexer": curShow.indexer, "tvdbid": curShow.ids.get(INDEXER_TVDB , {'id': 0})['id'], + 'ids': {k: v.get('id') for k, v in curShow.ids.iteritems()}, "tvrage_id": curShow.ids.get(INDEXER_TVRAGE, {'id': 0})['id'], "tvrage_name": curShow.name, "network": curShow.network, "show_name": curShow.name, "status": curShow.status, "subtitles": curShow.subtitles, + "scenenumbering": curShow.is_scene, + "archivefirstmatch": curShow.upgrade_once, + "irgnorewords": curShow.rls_ignore_words, + "requirewords": curShow.rls_require_words, + "tag": curShow.tag, + "imdb_id": curShow.imdbid, + "classification": curShow.classification, + "runtime": curShow.runtime, + "startyear": curShow.startyear, } + if self.overview: + showDict["overview"] = curShow.overview + + timezone, showDict['timezone'] = network_timezones.get_network_timezone(showDict['network'], + return_name=True) + if curShow.nextaired: - dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(curShow.nextaired, curShow.airs, showDict['network'])) + dtEpisodeAirs = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time( + curShow.nextaired, curShow.airs, timezone)) showDict['next_ep_airdate'] = sbdatetime.sbdatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat) else: showDict['next_ep_airdate'] = '' @@ -2636,9 +4161,120 @@ class CMD_Shows(ApiCall): return _responds(RESULT_SUCCESS, shows) -class CMD_ShowsStats(ApiCall): +class CMD_Shows(CMD_SickGearShows): + _help = {"desc": "display all thetvdb.com shows in sickgear", + "optionalParameters": {"sort": {"desc": "sort the list of shows by show name instead of indexerid"}, + "paused": {"desc": "only show the shows that are set to paused"}, + "overview": {"desc": "include overview"}, + }, + "SickGearCommand": "sg.shows", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearShows.__init__(self, handler, args, kwargs) + + +class CMD_SickGearShowsBrowseTrakt(ApiCall): + _help = {"desc": "browse trakt shows in sickgear", + "requiredParameters": {"type": {"desc": "type to browse: anticipated, newshows, newseasons, popular, " + "trending, recommended, watchlist"}, + }, + "optionalParameters": {"account_id": {"desc": "account_id for recommended, watchlist - " + "see sg.listtraktaccounts"}, + }, + } + + def __init__(self, handler, args, kwargs): + # required + self.type, args = self.check_params(args, kwargs, "type", "anticipated", True, "string", + ["anticipated", "newshows", "newseasons", "popular", "trending", + "recommended", "watchlist"]) + # optional + self.account, args = self.check_params(args, kwargs, "account_id", None, False, "int", + [s for s in sickbeard.TRAKT_ACCOUNTS]) + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ browse trakt shows in sickgear """ + urls = {'anticipated': 'shows/anticipated?limit=%s&' % 100, + 'newshows': '/calendars/all/shows/new/%s/%s?' % (sbdatetime.sbdatetime.sbfdate( + dt=datetime.datetime.now() + datetime.timedelta(days=-16), d_preset='%Y-%m-%d'), 32), + 'newseasons': '/calendars/all/shows/premieres/%s/%s?' % (sbdatetime.sbdatetime.sbfdate( + dt=datetime.datetime.now() + datetime.timedelta(days=-16), d_preset='%Y-%m-%d'), 32), + 'popular': 'shows/popular?limit=%s&' % 100, + 'trending': 'shows/trending?limit=%s&' % 100, + 'recommended': 'recommendations/shows?limit=%s&' % 100, + } + kwargs = {} + if self.type in ('recommended', 'watchlist'): + if not self.account: + return _responds(RESULT_FAILURE, msg='Need Trakt account') + kwargs['send_oauth'] = self.account + urls['watchlist'] = 'users/%s/watchlist/shows?limit=%s&' % (sickbeard.TRAKT_ACCOUNTS[self.account].slug, 100) + try: + data, oldest, newest = NewHomeAddShows.get_trakt_data(urls[self.type], **kwargs) + except Exception as e: + return _responds(RESULT_FAILURE, msg=e.message) + return _responds(RESULT_SUCCESS, data) + + +class CMD_SickGearListTraktAccounts(ApiCall): + _help = {"desc": "list Trakt accounts in sickgear"} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ list Trakt accounts in sickgear """ + accounts = [{'name': v.name, 'account_id': v.account_id} for a, v in sickbeard.TRAKT_ACCOUNTS.iteritems()] + return _responds(RESULT_SUCCESS, accounts) + + +class CMD_SickGearShowsForceUpdate(ApiCall): + _help = {"desc": "force the daily show update now."} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ force the daily show update now """ + if sickbeard.showQueueScheduler.action.isShowUpdateRunning() or sickbeard.showUpdateScheduler.action.amActive: + return _responds(RESULT_FAILURE, msg="show update already running.") + + result = sickbeard.showUpdateScheduler.forceRun() + if result: + return _responds(RESULT_SUCCESS, msg="daily show update started") + return _responds(RESULT_FAILURE, msg="can't start show update currently") + + +class CMD_SickGearShowsQueue(ApiCall): + _help = {"desc": "list the show update queue."} + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + ApiCall.__init__(self, handler, args, kwargs) + + def run(self): + """ list the show update queue """ + return _responds(RESULT_SUCCESS, sickbeard.showQueueScheduler.action.queue_length()) + + +class CMD_SickGearShowsStats(ApiCall): _help = {"desc": "display the global shows and episode stats" - } + } def __init__(self, handler, args, kwargs): # required @@ -2650,68 +4286,148 @@ class CMD_ShowsStats(ApiCall): """ display the global shows and episode stats """ stats = {} + indexer_limit = ('', ' AND indexer = %s' % INDEXER_TVDB)[self.sickbeard_call] myDB = db.DBConnection() today = str(datetime.date.today().toordinal()) - stats["shows_total"] = len(sickbeard.showList) + stats["shows_total"] = (len(sickbeard.showList), + len([x for x in sickbeard.showList if x.indexer == INDEXER_TVDB]))[self.sickbeard_call] stats["shows_active"] = len( - [show for show in sickbeard.showList if show.paused == 0 and show.status != "Ended"]) + [show for show in sickbeard.showList if show.paused == 0 and show.status != "Ended" + and (not self.sickbeard_call or show.indexer == INDEXER_TVDB)]) stats["ep_downloaded"] = myDB.select("SELECT COUNT(*) FROM tv_episodes WHERE status IN (" + ",".join( [str(show) for show in - Quality.DOWNLOADED + Quality.ARCHIVED]) + ") AND season != 0 and episode != 0 AND airdate <= " + today + "")[0][ - 0] + Quality.DOWNLOADED + Quality.ARCHIVED]) + ") AND season != 0 and episode != 0 AND airdate <= " + today + + indexer_limit)[0][0] stats["ep_total"] = myDB.select( "SELECT COUNT(*) FROM tv_episodes WHERE season != 0 AND episode != 0 AND (airdate != 1 OR status IN (" + ",".join( [str(show) for show in Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED]) + - ")) AND airdate <= " + today + " AND status != " + str(IGNORED) + "")[0][0] + ")) AND airdate <= " + today + " AND status != " + str(IGNORED) + indexer_limit)[0][0] return _responds(RESULT_SUCCESS, stats) + +class CMD_ShowsStats(CMD_SickGearShowsStats): + _help = {"desc": "display the global thetvdb.com shows and episode stats", + "SickGearCommand": "sg.shows.stats", + } + + def __init__(self, handler, args, kwargs): + # required + # optional + # super, missing, help + self.sickbeard_call = True + CMD_SickGearShowsStats.__init__(self, handler, args, kwargs) + + # WARNING: never define a cmd call string that contains a "_" (underscore) # this is reserved for cmd indexes used while cmd chaining # WARNING: never define a param name that contains a "." (dot) # this is reserved for cmd namspaces used while cmd chaining + + _functionMaper = {"help": CMD_Help, + "listcommands": CMD_ListCommands, "future": CMD_ComingEpisodes, + "sg.future": CMD_SickGearComingEpisodes, "episode": CMD_Episode, + "sg.episode": CMD_SickGearEpisode, "episode.search": CMD_EpisodeSearch, + "sg.episode.search": CMD_SickGearEpisodeSearch, "episode.setstatus": CMD_EpisodeSetStatus, + "sg.episode.setstatus": CMD_SickGearEpisodeSetStatus, "episode.subtitlesearch": CMD_SubtitleSearch, + "sg.episode.subtitlesearch": CMD_SickGearSubtitleSearch, "exceptions": CMD_Exceptions, + "sg.exceptions": CMD_SickGearExceptions, + 'sg.setexceptions': CMD_SetExceptions, "history": CMD_History, + "sg.history": CMD_SickGearHistory, "history.clear": CMD_HistoryClear, + "sg.history.clear": CMD_SickGearHistoryClear, "history.trim": CMD_HistoryTrim, + "sg.history.trim": CMD_SickGearHistoryTrim, "logs": CMD_Logs, + "sg.logs": CMD_Logs, "sb": CMD_SickBeard, + "sg": CMD_SickGear, "postprocess": CMD_PostProcess, + "sg.postprocess": CMD_SickGearPostProcess, "sb.addrootdir": CMD_SickBeardAddRootDir, + "sg.addrootdir": CMD_SickGearAddRootDir, "sb.checkscheduler": CMD_SickBeardCheckScheduler, + "sg.checkscheduler": CMD_SickGearCheckScheduler, "sb.deleterootdir": CMD_SickBeardDeleteRootDir, + "sg.deleterootdir": CMD_SickGearDeleteRootDir, "sb.forcesearch": CMD_SickBeardForceSearch, + "sg.forcesearch": CMD_SickGearForceSearch, + "sg.searchqueue": CMD_SickGearSearchQueue, "sb.getdefaults": CMD_SickBeardGetDefaults, + "sg.getdefaults": CMD_SickGearGetDefaults, "sb.getmessages": CMD_SickBeardGetMessages, + "sg.getmessages": CMD_SickGearGetMessages, + "sg.getqualities": CMD_SickGearGetQualities, + "sg.getqualitystrings": CMD_SickGearGetqualityStrings, "sb.getrootdirs": CMD_SickBeardGetRootDirs, + "sg.getrootdirs": CMD_SickGearGetRootDirs, "sb.pausebacklog": CMD_SickBeardPauseBacklog, + "sg.pausebacklog": CMD_SickGearPauseBacklog, "sb.ping": CMD_SickBeardPing, + "sg.ping": CMD_SickGearPing, "sb.restart": CMD_SickBeardRestart, + "sg.restart": CMD_SickGearRestart, "sb.searchtvdb": CMD_SickBeardSearchIndexers, + "sg.searchtv": CMD_SickGearSearchIndexers, "sb.setdefaults": CMD_SickBeardSetDefaults, + "sg.setscenenumber": CMD_SickGearSetSceneNumber, + "sg.activatescenenumbering": CMD_SickGearActivateSceneNumber, + "sg.getindexers": CMD_SickGearGetIndexers, + "sg.getindexericon": CMD_SickGearGetIndexerIcon, + "sg.getnetworkicon": CMD_SickGearGetNetworkIcon, "sb.shutdown": CMD_SickBeardShutdown, + "sg.listignorewords": CMD_SickGearListIgnoreWords, + "sg.setignorewords": CMD_SickGearSetIgnoreWords, + "sg.listrequiredwords": CMD_SickGearListRequireWords, + "sg.setrequiredwords": CMD_SickGearSetRequrieWords, "show": CMD_Show, + "sg.show": CMD_SickGearShow, "show.addexisting": CMD_ShowAddExisting, + "sg.show.addexisting": CMD_SickGearShowAddExisting, "show.addnew": CMD_ShowAddNew, + "sg.show.addnew": CMD_SickGearShowAddNew, "show.cache": CMD_ShowCache, + "sg.show.cache": CMD_SickGearShowCache, "show.delete": CMD_ShowDelete, + "sg.show.delete": CMD_SickGearShowDelete, "show.getquality": CMD_ShowGetQuality, + "sg.show.getquality": CMD_SickGearShowGetQuality, "show.getposter": CMD_ShowGetPoster, + "sg.show.getposter": CMD_SickGearShowGetPoster, "show.getbanner": CMD_ShowGetBanner, + "sg.show.getbanner": CMD_SickGearShowGetBanner, + "sg.show.listfanart": CMD_SickGearShowListFanart, + "sg.show.ratefanart": CMD_SickGearShowRateFanart, + "sg.show.getfanart": CMD_SickGearShowGetFanart, "show.pause": CMD_ShowPause, + "sg.show.pause": CMD_SickGearShowPause, "show.refresh": CMD_ShowRefresh, + "sg.show.refresh": CMD_SickGearShowRefresh, "show.seasonlist": CMD_ShowSeasonList, + "sg.show.seasonlist": CMD_SickGearShowSeasonList, "show.seasons": CMD_ShowSeasons, + "sg.show.seasons": CMD_SickGearShowSeasons, "show.setquality": CMD_ShowSetQuality, + "sg.show.setquality": CMD_SickGearShowSetQuality, "show.stats": CMD_ShowStats, + "sg.show.stats": CMD_SickGearShowStats, "show.update": CMD_ShowUpdate, + "sg.show.update": CMD_SickGearShowUpdate, "shows": CMD_Shows, - "shows.stats": CMD_ShowsStats -} + "sg.shows": CMD_SickGearShows, + "sg.shows.browsetrakt": CMD_SickGearShowsBrowseTrakt, + "sg.listtraktaccounts": CMD_SickGearListTraktAccounts, + "shows.stats": CMD_ShowsStats, + "sg.shows.stats": CMD_SickGearShowsStats, + "sg.shows.forceupdate": CMD_SickGearShowsForceUpdate, + "sg.shows.queue": CMD_SickGearShowsQueue, + } diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 19919e62..1e37879e 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -51,7 +51,7 @@ from sickbeard.helpers import has_image_ext, remove_article, starify from sickbeard.indexers.indexer_config import INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_TRAKT from sickbeard.scene_numbering import get_scene_numbering, set_scene_numbering, get_scene_numbering_for_show, \ get_xem_numbering_for_show, get_scene_absolute_numbering_for_show, get_xem_absolute_numbering_for_show, \ - get_scene_absolute_numbering + get_scene_absolute_numbering, set_scene_numbering_helper from sickbeard.name_cache import buildNameCache from sickbeard.browser import foldersAtPath from sickbeard.blackandwhitelist import BlackAndWhiteList, short_group_names @@ -145,6 +145,13 @@ class BaseHandler(RequestHandler): return self.get_secure_cookie('sickgear-session-%s' % helpers.md5_for_text(sickbeard.WEB_PORT)) return True + def getImage(self, image): + if ek.ek(os.path.isfile, image): + mime_type, encoding = MimeTypes().guess_type(image) + self.set_header('Content-Type', mime_type) + with ek.ek(open, image, 'rb') as img: + return img.read() + def showPoster(self, show=None, which=None, api=None): # Redirect initial poster/banner thumb to default images if 'poster' == which[0:6]: @@ -175,9 +182,14 @@ class BaseHandler(RequestHandler): static_image_path = image_file_name if api: + used_file = ek.ek(os.path.basename, static_image_path) + if static_image_path.startswith('/images'): + used_file = 'default' + static_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', static_image_path[1:]) mime_type, encoding = MimeTypes().guess_type(static_image_path) self.set_header('Content-Type', mime_type) - with open(static_image_path, 'rb') as img: + self.set_header('X-Filename', used_file) + with ek.ek(open, static_image_path, 'rb') as img: return img.read() else: static_image_path = os.path.normpath(static_image_path.replace(sickbeard.CACHE_DIR, '/cache')) @@ -2458,50 +2470,8 @@ class Home(MainHandler): def setSceneNumbering(self, show, indexer, forSeason=None, forEpisode=None, forAbsolute=None, sceneSeason=None, sceneEpisode=None, sceneAbsolute=None): - # sanitize: - show = None if show in [None, 'null', ''] else int(show) - indexer = None if indexer in [None, 'null', ''] else int(indexer) - - show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, show) - - if not show_obj.is_anime: - for_season = None if forSeason in [None, 'null', ''] else int(forSeason) - for_episode = None if forEpisode in [None, 'null', ''] else int(forEpisode) - scene_season = None if sceneSeason in [None, 'null', ''] else int(sceneSeason) - scene_episode = None if sceneEpisode in [None, 'null', ''] else int(sceneEpisode) - action_log = u'Set episode scene numbering to %sx%s for episode %sx%s of "%s"'\ - % (scene_season, scene_episode, for_season, for_episode, show_obj.name) - ep_args = {'show': show, 'season': for_season, 'episode': for_episode} - scene_args = {'indexer_id': show, 'indexer': indexer, 'season': for_season, 'episode': for_episode, - 'sceneSeason': scene_season, 'sceneEpisode': scene_episode} - result = {'forSeason': for_season, 'forEpisode': for_episode, 'sceneSeason': None, 'sceneEpisode': None} - else: - for_absolute = None if forAbsolute in [None, 'null', ''] else int(forAbsolute) - scene_absolute = None if sceneAbsolute in [None, 'null', ''] else int(sceneAbsolute) - action_log = u'Set absolute scene numbering to %s for episode %s of "%s"'\ - % (scene_absolute, for_absolute, show_obj.name) - ep_args = {'show': show, 'absolute': for_absolute} - scene_args = {'indexer_id': show, 'indexer': indexer, 'absolute_number': for_absolute, - 'sceneAbsolute': scene_absolute} - result = {'forAbsolute': for_absolute, 'sceneAbsolute': None} - - ep_obj = self._getEpisode(**ep_args) - result['success'] = not isinstance(ep_obj, str) - if result['success']: - logger.log(action_log, logger.DEBUG) - set_scene_numbering(**scene_args) - show_obj.flushEpisodes() - else: - result['errorMessage'] = ep_obj - - if not show_obj.is_anime: - scene_numbering = get_scene_numbering(show, indexer, for_season, for_episode) - if scene_numbering: - (result['sceneSeason'], result['sceneEpisode']) = scene_numbering - else: - scene_numbering = get_scene_absolute_numbering(show, indexer, for_absolute) - if scene_numbering: - result['sceneAbsolute'] = scene_numbering + result = set_scene_numbering_helper(show, indexer, forSeason, forEpisode, forAbsolute, sceneSeason, + sceneEpisode, sceneAbsolute) return json.dumps(result) @@ -2730,7 +2700,7 @@ class NewHomeAddShows(Home): re.sub(r'([,.!][^,.!]*?)$', '...', re.sub(r'([.!?])(?=\w)', r'\1 ', self.encode_html((show.get('overview', '') or '')[:250:].strip()))), - self._get_UWRatio(term, show['seriesname'], show.get('aliases', [])), None, None, + self.get_UWRatio(term, show['seriesname'], show.get('aliases', [])), None, None, self._make_search_image_url(iid, show) ] for show in shows.itervalues()] for iid, shows in results.iteritems())) @@ -2791,7 +2761,8 @@ class NewHomeAddShows(Home): ('%s.jpg' % show['id'], show['id']) return img_url - def _get_UWRatio(self, search_term, showname, aliases): + @classmethod + def get_UWRatio(cls, search_term, showname, aliases): s = fuzz.UWRatio(search_term, showname) # check aliases and give them a little lower score for a in aliases: @@ -3452,6 +3423,93 @@ class NewHomeAddShows(Home): return self.redirect('/home/addShows/%s' % ('trakt_trending', sickbeard.TRAKT_MRU)[any(sickbeard.TRAKT_MRU)]) + @staticmethod + def get_trakt_data(url_path, *args, **kwargs): + normalised, filtered = ([], []) + error_msg = None + try: + account = kwargs.get('send_oauth', None) + if account: + account = sickbeard.helpers.tryInt(account) + resp = TraktAPI().trakt_request('%sextended=full,images' % url_path, send_oauth=account) + if resp: + if 'show' in resp[0]: + if 'first_aired' in resp[0]: + for item in resp: + item['show']['first_aired'] = item['first_aired'] + del item['first_aired'] + normalised = resp + else: + for item in resp: + normalised.append({u'show': item}) + del resp + except TraktAuthException as e: + logger.log(u'Pin authorisation needed to connect to Trakt service: %s' % ex(e), logger.WARNING) + error_msg = 'Unauthorized: Get another pin in the Notifications Trakt settings' + except TraktException as e: + logger.log(u'Could not connect to Trakt service: %s' % ex(e), logger.WARNING) + except (IndexError, KeyError): + pass + + if not normalised: + error_msg = 'No items in watchlist. Use the "Add to watchlist" button at the Trakt website' + raise Exception(error_msg) + + oldest_dt = 9999999 + newest_dt = 0 + oldest = None + newest = None + for item in normalised: + ignore = ''' + ((bbc|channel\s*?5.*?|itv)\s*?(drama|documentaries))|bbc\s*?(comedy|music)|music\s*?specials|tedtalks + ''' + if re.search(ignore, item['show']['title'].strip(), re.I | re.X): + continue + try: + dt = dateutil.parser.parse(item['show']['first_aired']) + dt_ordinal = dt.toordinal() + dt_string = sbdatetime.sbdatetime.sbfdate(dt) + if dt_ordinal < oldest_dt: + oldest_dt = dt_ordinal + oldest = dt_string + if dt_ordinal > newest_dt: + newest_dt = dt_ordinal + newest = dt_string + + tmdbid = item.get('show', {}).get('ids', {}).get('tmdb', 0) + tvdbid = item.get('show', {}).get('ids', {}).get('tvdb', 0) + traktid = item.get('show', {}).get('ids', {}).get('trakt', 0) + images = dict(poster=dict(thumb='imagecache?path=browse/thumb/trakt&filename=%s&tmdbid=%s&tvdbid=%s' % + ('%s.jpg' % traktid, tmdbid, tvdbid))) + + filtered.append(dict( + premiered=dt_ordinal, + premiered_str=dt_string, + when_past=dt_ordinal < datetime.datetime.now().toordinal(), # air time not yet available 16.11.2015 + episode_number='' if 'episode' not in item else item['episode']['number'] or 1, + episode_overview=('' if 'episode' not in item else + item['episode']['overview'].strip() or ''), + episode_season='' if 'episode' not in item else item['episode']['season'] or 1, + genres=('' if 'genres' not in item['show'] else + ', '.join(['%s' % v for v in item['show']['genres']])), + ids=item['show']['ids'], + images=images, + overview=('' if 'overview' not in item['show'] or None is item['show']['overview'] else + item['show']['overview'].strip()), + rating=0 < item['show'].get('rating', 0) and + ('%.2f' % (item['show'].get('rating') * 10)).replace('.00', '') or 0, + title=item['show']['title'].strip(), + url_src_db='https://trakt.tv/shows/%s' % item['show']['ids']['slug'], + url_tvdb=('', '%s%s' % (sickbeard.indexerApi(INDEXER_TVDB).config['show_url'], + item['show']['ids']['tvdb']))[ + isinstance(item['show']['ids']['tvdb'], (int, long)) + and 0 < item['show']['ids']['tvdb']], + votes='0' if 'votes' not in item['show'] else item['show']['votes'])) + except (StandardError, Exception): + pass + + return filtered, oldest, newest + def browse_trakt(self, url_path, browse_title, *args, **kwargs): browse_type = 'Trakt' @@ -6233,6 +6291,8 @@ class ApiBuilder(MainHandler): t.seasonSQLResults = seasonSQLResults t.episodeSQLResults = episodeSQLResults + t.indexers = sickbeard.indexerApi().all_indexers + t.searchindexers = sickbeard.indexerApi().search_indexers if len(sickbeard.API_KEY) == 32: t.apikey = sickbeard.API_KEY From 9abc7e0105abe0cd635cfa0f8f918f3c33d1981f Mon Sep 17 00:00:00 2001 From: JackDandy Date: Fri, 2 Feb 2018 03:14:31 +0000 Subject: [PATCH 2/2] Change DRY the browse_trakt function that was duplicated with slight changes for API. --- .../interfaces/default/config_general.tmpl | 4 +- sickbeard/webserve.py | 88 +++---------------- 2 files changed, 14 insertions(+), 78 deletions(-) diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl index 20fb0697..6f2db9c2 100644 --- a/gui/slick/interfaces/default/config_general.tmpl +++ b/gui/slick/interfaces/default/config_general.tmpl @@ -481,8 +481,7 @@ API enable -

permit the use of the SickGear (SickBeard) API

-

Old Sickbeard API calls are limited to shows from thetvdb.com. Use new API LINK for full access

+

permit the use of the SickGear (and Legacy SickBeard) API

@@ -491,6 +490,7 @@