diff --git a/.gitignore b/.gitignore index f081a23927ace05633476a242501b46c4c126988..2698b0051deb51173074222a3a739dec1de4e6de 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ ###################### cache/* cache.db* -config.ini +config.ini* Logs/* sickbeard.db* autoProcessTV/autoProcessTV.cfg diff --git a/data/interfaces/default/apiBuilder.tmpl b/data/interfaces/default/apiBuilder.tmpl index f6a1dd5570d9bc10f8ad170648497af7e0fe9fb2..09d66dc9d7c5e323b64946c636210478599f2cec 100644 --- a/data/interfaces/default/apiBuilder.tmpl +++ b/data/interfaces/default/apiBuilder.tmpl @@ -90,8 +90,8 @@ addList("sb.setdefaults-status", "Archived", "&status=archived", "sb.setdefaults addList("sb.setdefaults-status", "Ignored", "&status=ignored", "sb.setdefaults-opt"); addOption("sb.setdefaults-opt", "Optional Param", "", 1); -addList("sb.setdefaults-opt", "No Season Folder", "&season_folder=0", "quality"); -addList("sb.setdefaults-opt", "Use Season Folder", "&season_folder=1", "quality"); +addList("sb.setdefaults-opt", "Flatten (No Season Folder)", "&flatten_folders=1", "quality"); +addList("sb.setdefaults-opt", "Use Season Folder", "&flatten_folders=0", "quality"); addOption("shows", "Optional Param", "", 1); addOption("shows", "Show Only Paused", "&paused=1"); @@ -108,8 +108,8 @@ addList("show.addexisting-tvdbid", "101501 (Ancient Aliens)", "&tvdbid=101501", addList("show.addexisting-tvdbid", "80348 (Chuck)", "&tvdbid=80348", "show.addexisting-opt"); addOption("show.addexisting-opt", "Optional Param", "", 1); -addList("show.addexisting-opt", "No Season Folder", "&season_folder=0", "quality"); -addList("show.addexisting-opt", "Use Season Folder", "&season_folder=1", "quality"); +addList("show.addexisting-opt", "Flatten (No Season Folder)", "&flatten_folders=1", "quality"); +addList("show.addexisting-opt", "Use Season Folder", "&flatten_folders=0", "quality"); addList("show.addnew", "101501 (Ancient Aliens)", "&tvdbid=101501", "show.addnew-loc"); addList("show.addnew", "80348 (Chuck)", "&tvdbid=80348", "show.addnew-loc"); @@ -127,8 +127,8 @@ addList("show.addnew-status", "Archived", "&status=archived", "show.addnew-opt") addList("show.addnew-status", "Ignored", "&status=ignored", "show.addnew-opt"); addOption("show.addnew-opt", "Optional Param", "", 1); -addList("show.addnew-opt", "No Season Folder", "&season_folder=0", "quality"); -addList("show.addnew-opt", "Use Season Folder", "&season_folder=1", "quality"); +addList("show.addnew-opt", "Flatten (No Season Folder)", "&flatten_folders=1", "quality"); +addList("show.addnew-opt", "Use Season Folder", "&flatten_folders=0", "quality"); addOptGroup("sb.searchtvdb", "Search by Name"); addList("sb.searchtvdb", "Lost", "&name=Lost", "sb.searchtvdb-lang"); @@ -258,15 +258,25 @@ addList("episode.setstatus", "$curShow.name", "&tvdbid=$curShow.tvdbid", "episod // build out each show's season+episode list for episode.setstatus cmd #for $curShow in $episodeSQLResults: + #set $curSeason = -1 #for $curShowSeason in $episodeSQLResults[$curShow]: + #if $curShowSeason.season != $curSeason and $curShowSeason.season != 0: + // insert just the season as the ep number is now optional + addList("episode.setstatus-$curShow", "Season $curShowSeason.season", "&season=$curShowSeason.season", "episode-status-$curShow"); + #end if + #set $curSeason = int($curShowSeason.season) addList("episode.setstatus-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "episode-status-$curShow"); #end for -addOption("episode-status-$curShow", "Wanted", "&status=wanted"); -addOption("episode-status-$curShow", "Skipped", "&status=skipped"); -addOption("episode-status-$curShow", "Archived", "&status=archived"); -addOption("episode-status-$curShow", "Ignored", "&status=ignored"); +addList("episode-status-$curShow", "Wanted", "&status=wanted", "force"); +addList("episode-status-$curShow", "Skipped", "&status=skipped", "force"); +addList("episode-status-$curShow", "Archived", "&status=archived", "force"); +addList("episode-status-$curShow", "Ignored", "&status=ignored", "force"); #end for +addOption("force", "Optional Param", "", 1); +addOption("force", "Replace Downloaded EP", "&force=1"); +addOption("force", "Skip Downloaded EP", "&force=0"); + addOption("future", "Optional Param", "", 1); addList("future", "Sort by Date", "&sort=date", "future-type"); addList("future", "Sort by Network", "&sort=network", "future-type"); diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 06e636531aba8507e64bac58caaa2c0ad1acd8c0..b9b543e4d69d6f5ae7af444559658f35213609fa 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -116,19 +116,20 @@ class TVShow(object): self.episodes[curSeason][curEp] = None del myEp + def getAllEpisodes(self, season=None): - def getAllEpisodes(self): - myDB = db.DBConnection() - results = myDB.select("SELECT season, episode FROM tv_episodes WHERE showid = ?", [self.tvdbid]) + if season == None: + results = myDB.select("SELECT season, episode FROM tv_episodes WHERE showid = ?", [self.tvdbid]) + else: + results = myDB.select("SELECT season, episode FROM tv_episodes WHERE showid = ? AND season = ?", [self.tvdbid, season]) ep_list = [] - for cur_result in results: cur_ep = self.getEpisode(int(cur_result["season"]), int(cur_result["episode"])) if cur_ep: ep_list.append(cur_ep) - + return ep_list diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py index a7e005e06c03bfbe09073adcf73e21a32f0dcbef..f66f641b87ae8027ba60ec7d210131b48bc5d9ea 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/webapi.py @@ -67,7 +67,7 @@ result_type_map = {RESULT_SUCCESS: "success", class Api: """ api class that returns json results """ - version = 0.2 + version = 0.3 intent = 4 @cherrypy.expose @@ -167,7 +167,7 @@ class Api: callback = request.params.get('callback') or request.params.get('jsonp') if callback != None: out = callback + '(' + out + ');' # wrap with JSONP call if requested - except Exception, e: # if we fail to generate the output fake a error + except Exception, e: # if we fail to generate the output fake an error logger.log(u"API :: " + traceback.format_exc(), logger.DEBUG) out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex(e) + '"}' return out @@ -493,6 +493,13 @@ class TVDBShorthandWrapper(ApiCall): # helper functions # ################################ +def _sizeof_fmt(num): + for x in ['bytes', 'KB', 'MB', 'GB', 'TB']: + if num < 1024.00: + return "%3.2f %s" % (num, x) + num /= 1024.00 + + def _is_int(data): try: int(data) @@ -799,7 +806,7 @@ class CMD_Episode(ApiCall): return _responds(RESULT_FAILURE, msg="Show not found") myDB = db.DBConnection(row_type="dict") - sqlResults = myDB.select("SELECT name, description, airdate, status, location FROM tv_episodes WHERE showid = ? AND episode = ? AND season = ?", [self.tvdbid, self.e, self.s]) + sqlResults = myDB.select("SELECT name, description, airdate, status, location, file_size, release_name FROM tv_episodes WHERE showid = ? AND episode = ? AND season = ?", [self.tvdbid, self.e, self.s]) if not len(sqlResults) == 1: raise ApiError("Episode not found") episode = sqlResults[0] @@ -814,7 +821,7 @@ class CMD_Episode(ApiCall): if bool(self.fullPath) == True and showPath: pass elif bool(self.fullPath) == False and showPath: - #i am using the length because lstrip removes to much + # using the length because lstrip removes to much showPathLength = len(showPath) + 1 # the / or \ yeah not that nice i know episode["location"] = episode["location"][showPathLength:] elif not showPath: # show dir is broken ... episode path will be empty @@ -824,6 +831,7 @@ class CMD_Episode(ApiCall): status, quality = Quality.splitCompositeStatus(int(episode["status"])) episode["status"] = _get_status_Strings(status) episode["quality"] = _get_quality_string(quality) + episode["file_size_human"] = _sizeof_fmt(episode["file_size"]) myDB.connection.close() return _responds(RESULT_SUCCESS, episode) @@ -875,26 +883,29 @@ class CMD_EpisodeSearch(ApiCall): class CMD_EpisodeSetStatus(ApiCall): - _help = {"desc": "set status of an episode", + _help = {"desc": "set status of an episode or season (when no ep is provided)", "requiredParameters": {"tvdbid": {"desc": "thetvdb.com unique id of a show"}, "season": {"desc": "the season number"}, - "episode": {"desc": "the episode number"}, "status": {"desc": "the status values: wanted, skipped, archived, ignored"} - } + }, + "optionalParameters": {"episode": {"desc": "the episode number"}, + "force": {"desc": "should we replace existing (downloaded) episodes or not"} + } } def __init__(self, args, kwargs): # required self.tvdbid, args = self.check_params(args, kwargs, "tvdbid", 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", []) self.status, args = self.check_params(args, kwargs, "status", None, True, "string", ["wanted", "skipped", "archived", "ignored"]) # 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", []) # super, missing, help ApiCall.__init__(self, args, kwargs) def run(self): - """ set status of an episode """ + """ set status of an episode or a season (when no ep is provided) """ showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(self.tvdbid)) if not showObj: return _responds(RESULT_FAILURE, msg="Show not found") @@ -904,47 +915,67 @@ class CMD_EpisodeSetStatus(ApiCall): if str(statusStrings[status]).lower() == str(self.status).lower(): self.status = status break - # this should be obsolete bcause of the above - if not self.status in statusStrings.statusStrings: - return _responds(RESULT_FAILURE, msg="Invalid Status") + else: # if we dont break out of the for loop we got here. + # 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!") - epObj = showObj.getEpisode(int(self.s), int(self.e)) - if epObj == None: - return _responds(RESULT_FAILURE, msg="Episode not found") - - #only allow the status options we want - if int(self.status) not in (3, 5, 6, 7): - return _responds(RESULT_FAILURE, msg="Show not found") - - segment_list = [] - if int(self.status) == WANTED: - # figure out what segment the episode is in and remember it so we can backlog it - if epObj.show.air_by_date: - ep_segment = str(epObj.airdate)[:7] - else: - ep_segment = epObj.season - - if ep_segment not in segment_list: - segment_list.append(ep_segment) + ep_list = [] + if self.e: + epObj = showObj.getEpisode(self.s, self.e) + if epObj == None: + return _responds(RESULT_FAILURE, msg="Episode not found") + ep_list = [epObj] + else: + # get all episode numbers frome self,season + ep_list = showObj.getAllEpisodes(season=self.s) + + def _epResult(result_code, ep, msg=""): + return {'season': ep.season, 'episode': ep.episode, 'status': _get_status_Strings(ep.status), 'result': result_type_map[result_code], 'message': msg} + + ep_results = [] + failure = False + start_backlog = False + ep_segment = None + for epObj in ep_list: + if ep_segment == None and self.status == WANTED: + # figure out what segment the episode is in and remember it so we can backlog it + if showObj.air_by_date: + ep_segment = str(epObj.airdate)[:7] + else: + ep_segment = epObj.season + + with epObj.lock: + # don't let them mess up UNAIRED episodes + if epObj.status == UNAIRED: + if self.e != None: # setting the status of a unaired is only considert a failure if we directly wanted this episode, but is ignored on a season request + ep_results.append(_epResult(RESULT_FAILURE, epObj, "Refusing to change status because it is UNAIRED")) + failure = True + continue - with epObj.lock: - # don't let them mess up UNAIRED episodes - if epObj.status == UNAIRED: - return _responds(RESULT_FAILURE, msg="Refusing to change status because it is UNAIRED") + # allow the user to force setting the status for an already downloaded episode + if epObj.status in Quality.DOWNLOADED and not self.force: + ep_results.append(_epResult(RESULT_FAILURE, epObj, "Refusing to change status because it is already marked as DOWNLOADED")) + failure = True + continue - if int(self.status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED + [IGNORED] and not ek.ek(os.path.isfile, epObj.location): - return _responds(RESULT_FAILURE, msg="Refusing to change status to DOWNLOADED because it's not SNATCHED/DOWNLOADED") + epObj.status = self.status + epObj.saveToDB() - epObj.status = int(self.status) - epObj.saveToDB() + if self.status == WANTED: + start_backlog = True + ep_results.append(_epResult(RESULT_SUCCESS, epObj)) - for cur_segment in segment_list: - cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, cur_segment) - sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable - logger.log(u"API :: Starting backlog for " + showObj.name + " season " + str(cur_segment) + " because some eps were set to wanted") - return _responds(RESULT_SUCCESS, msg="Episode status changed to Wanted, and backlog started") + extra_msg = "" + if start_backlog: + cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, ep_segment) + sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable + logger.log(u"API :: Starting backlog for " + showObj.name + " season " + str(ep_segment) + " because some episodes were set to WANTED") + extra_msg = " Backlog started" - return _responds(RESULT_SUCCESS, msg="Episode status successfully changed to " + statusStrings[epObj.status]) + if failure: + return _responds(RESULT_FAILURE, ep_results, 'Failed to set all or some status. Check data.' + extra_msg) + else: + return _responds(RESULT_SUCCESS, msg='All status set successfully.' + extra_msg) class CMD_Exceptions(ApiCall):