diff --git a/data/interfaces/default/displayShow.tmpl b/data/interfaces/default/displayShow.tmpl index fd47c0ab5044a24ba458380da62c6a3c19d89577..6020250bbb5bb9cb06a3fc94a8c9fc7137b617f7 100644 --- a/data/interfaces/default/displayShow.tmpl +++ b/data/interfaces/default/displayShow.tmpl @@ -1,178 +1,238 @@ -#import sickbeard -#import sickbeard.helpers -#from sickbeard.common import * -#import os.path, os -#import datetime -#set global $title=$show.name -#set global $header = '<a href="http://thetvdb.com/?tab=series&id=%d" target="_new">%s</a>' % ($show.tvdbid, $show.name) - -#set global $topmenu="manageShows"# -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/lib/jquery.bookmarkscroll.js?$sbPID"></script> - - -<div class="h2footer align-right"> -#if (len($seasonResults) > 14): - <select id="seasonJump"> - <option value="jump">Jump to Season</option> - #for $seasonNum in $seasonResults: - <option value="#season-$seasonNum["season"]">#if int($seasonNum["season"]) == 0 then "Specials" else "Season " + str($seasonNum["season"])#</option> - #end for - </select> -#else: - <b>Season:</b> - #for $seasonNum in $seasonResults: - #if int($seasonNum["season"]) == 0: - <a href="#season-$seasonNum["season"]">Specials</a> - #else: - <a href="#season-$seasonNum["season"]">${str($seasonNum["season"])}</a> - #end if - #if $seasonNum != $seasonResults[-1]: - <span class="separator">|</span> - #end if - #end for -#end if -</div><br/> - -#if $show_message: - <div class="alert alert-info"> - $show_message - </div> -#end if - -<input type="hidden" id="sbRoot" value="$sbRoot" /> - -<script type="text/javascript" src="$sbRoot/js/displayShow.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script> - -<div class="align-left"><b>Change Show:</b> -<div class="navShow"><img id="prevShow" width="16" height="18" src="$sbRoot/images/prev.gif" alt="<<" title="Prev Show" /></div> -<select id="pickShow"> -#for $curShow in $sortedShowList: -<option value="$curShow.tvdbid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option> -#end for -</select> -<div class="navShow"><img id="nextShow" width="16" height="18" src="$sbRoot/images/next.gif" alt=">>" title="Next Show" /></div> -</div> - -<div id="summary" class="align-left"> -<table> -#if $show.network and $show.airs: - <tr><td class="showLegend">Airs: </td><td>$show.airs on $show.network</td></tr> -#else if $show.network: - <tr><td class="showLegend">Airs: </td><td>$show.network</td></tr> -#else if $show.airs: - <tr><td class="showLegend">Airs: </td><td>$show.airs</td></tr> -#end if - <tr><td class="showLegend">Status: </td><td>$show.status</td></tr> -#if $showLoc[1]: - <tr><td class="showLegend">Location: </td><td>$showLoc[0]</td></tr> -#else: - <tr><td class="showLegend"><span style="color: red;">Location: </span></td><td><span style="color: red;">$showLoc[0]</span> (dir is missing)</td></tr> -#end if -#set $anyQualities, $bestQualities = $Quality.splitQuality(int($show.quality)) - <tr><td class="showLegend">Quality: </td><td> -#if $show.quality in $qualityPresets: -$qualityPresetStrings[$show.quality] -#else: -#if $anyQualities: -initially download: <b><%=", ".join([Quality.qualityStrings[x] for x in anyQualities])%></b> #if $bestQualities then " + " else ""# -#end if -#if $bestQualities: -replace with: <b><%=", ".join([Quality.qualityStrings[x] for x in bestQualities])%></b> -#end if -#end if - </td></tr> - <tr><td class="showLegend">Language:</td><td><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="" /> $show.lang</td></tr> - <tr><td class="showLegend">Flatten Folders: </td><td><img src="$sbRoot/images/#if $show.flatten_folders == 1 or $sickbeard.NAMING_FORCE_FOLDERS then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> - <tr><td class="showLegend">Paused: </td><td><img src="$sbRoot/images/#if int($show.paused) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> - <tr><td class="showLegend">Air-by-Date: </td><td><img src="$sbRoot/images/#if int($show.air_by_date) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> -</table> -</div> - -<div class="float-left"> -Change selected episodes to -<select id="statusSelect"> -#for $curStatus in [$WANTED, $SKIPPED, $ARCHIVED, $IGNORED] + $Quality.DOWNLOADED: -#if $curStatus == $DOWNLOADED: -#continue -#end if -<option value="$curStatus">$statusStrings[$curStatus]</option> -#end for -</select> -<input type="hidden" id="showID" value="$show.tvdbid" /> -<input type="button" class="btn" id="changeStatus" value="Go" /><br /> -<br /> -<br /> -</div> - -#set $curSeason = -1 - -<div class="float-right clearfix" id="checkboxControls"> - <div style="padding-bottom: 5px;"> - <label for="wanted" class="checkbox inline wanted"><input type="checkbox" id="wanted" checked="checked" /> Wanted: <b>$epCounts[$Overview.WANTED]</b></label> - <label for="qual" class="checkbox inline qual"><input type="checkbox" id="qual" checked="checked" /> Low Quality: <b>$epCounts[$Overview.QUAL]</b></label> - <label for="good" class="checkbox inline good"><input type="checkbox" id="good" checked="checked" /> Downloaded: <b>$epCounts[$Overview.GOOD]</b></label> - <label for="skipped" class="checkbox inline skipped"><input type="checkbox" id="skipped" checked="checked" /> Skipped: <b>$epCounts[$Overview.SKIPPED]</b></label> - </div> - <div class="pull-right"> - <button class="btn btn-mini seriesCheck"><a href="#" onclick="return false;">Select Filtered Episodes</a></button> - <button class="btn btn-mini clearAll"><a href="#" onclick="return false;">Clear All</a></button> - </div> -</div> - -<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> - -#for $epResult in $sqlResults: - - #if int($epResult["season"]) != $curSeason: - <tr><td colspan="9"><a name="season-$epResult["season"]"></a></td></tr> - <tr class="seasonheader" id="season-$epResult["season"]"> - <td colspan="9"> - <h2>#if int($epResult["season"]) == 0 then "Specials" else "Season "+str($epResult["season"])#</h2> - </td> - </tr> - <tr id="season-$epResult["season"]-cols"><th width="1%"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th><th>NFO</th><th>TBN</th><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th><th>Status</th><th>Search</th></tr> - #set $curSeason = int($epResult["season"]) - #end if - - #set $epStr = str($epResult["season"]) + "x" + str($epResult["episode"]) - #set $epLoc = $epResult["location"] - <tr class="$Overview.overviewStrings[$epCats[$epStr]] season-$curSeason"> - <td width="1%"> -#if int($epResult["status"]) != $UNAIRED - <input type="checkbox" class="epCheck" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" /> -#end if - </td> - <td align="center"><img src="$sbRoot/images/#if $epResult["hasnfo"] == 1 then "nfo.gif\" alt=\"Y" else "nfo-no.gif\" alt=\"N"#" width="23" height="11" /></td> - <td align="center"><img src="$sbRoot/images/#if $epResult["hastbn"] == 1 then "tbn.gif\" alt=\"Y" else "tbn-no.gif\" alt=\"N"#" width="23" height="11" /></td> - <td align="center">$epResult["episode"]</td> - <td> - $epResult["name"] - #if $epResult["description"] != "" and $epResult["description"] != None: - <img src="$sbRoot/images/info32.png" height="16" class="plotInfo" alt="" id="plot_info_$show.tvdbid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>" /> - #end if - </td> - <td align="center" class="nowrap">#if int($epResult["airdate"]) == 1 then "never" else $datetime.date.fromordinal(int($epResult["airdate"]))#</td> - <td> -#if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower()): -$epLoc[len($show._location)+1:] -#elif $epLoc and (not $epLoc.lower().startswith($show._location.lower()) or not $show._location): -$epLoc -#end if - - </td> - <td class="status_column">$statusStrings[int($epResult["status"])]</td> - <td align="center"> - #if int($epResult["season"]) != 0: - <a class="epSearch" href="searchEpisode?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/search16.png" height="16" alt="search" title="Manual Search" /></a> - #end if - </td> - </tr> - -#end for -</table><br /> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#import sickbeard.helpers +#from sickbeard.common import * +#from sickbeard import db +#import os.path, os +#import datetime +#set global $title=$show.name +#set global $header = '<a href="http://thetvdb.com/?tab=series&id=%d" target="_new">%s</a>' % ($show.tvdbid, $show.name) +#set $myDB = $db.DBConnection() +#set $today = str($datetime.date.today().toordinal()) +#set global $topmenu="manageShows"# +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#set $fr = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'fr' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") +#set $curfr = [x[1] for x in $fr if int(x[0]) == $show.tvdbid] +#if len($curfr) != 0: + #set $lfr = $curfr[0] +#else + #set $lfr = 0 +#end if +#set $en = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'en' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") +#set $curen = [x[1] for x in $en if int(x[0]) == $show.tvdbid] +#if len($curen) != 0: + #set $leng = $curen[0] +#else + #set $leng = 0 +#end if +#set $no = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = '' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") +#set $curno = [x[1] for x in $no if int(x[0]) == $show.tvdbid] +#if len($curno) != 0: + #set $lno = $curno[0] +#else + #set $lno = 0 +#end if +#set $manq = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE location = '' AND season != 0 and episode != 0 AND (airdate <= "+$today+" and airdate != 1) GROUP BY showid") +#set $curmanq = [x[1] for x in $manq if int(x[0]) == $show.tvdbid] +#if len($curmanq) != 0: + #set $lmanq = $curmanq[0] +#else + #set $lmanq = 0 +#end if +<script type="text/javascript" src="$sbRoot/js/lib/jquery.bookmarkscroll.js?$sbPID"></script> + + +<div class="h2footer align-right"> +#if (len($seasonResults) > 14): + <select id="seasonJump"> + <option value="jump">Jump to Season</option> + #for $seasonNum in $seasonResults: + <option value="#season-$seasonNum["season"]">#if int($seasonNum["season"]) == 0 then "Specials" else "Season " + str($seasonNum["season"])#</option> + #end for + </select> +#else: + <b>Season:</b> + #for $seasonNum in $seasonResults: + #if int($seasonNum["season"]) == 0: + <a href="#season-$seasonNum["season"]">Specials</a> + #else: + <a href="#season-$seasonNum["season"]">${str($seasonNum["season"])}</a> + #end if + #if $seasonNum != $seasonResults[-1]: + <span class="separator">|</span> + #end if + #end for +#end if +</div><br/> + +#if $show_message: + <div class="alert alert-info"> + $show_message + </div> +#end if + +<input type="hidden" id="sbRoot" value="$sbRoot" /> + +<script type="text/javascript" src="$sbRoot/js/displayShow.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script> + +<div class="align-left"><b>Change Show:</b> +<div class="navShow"><img id="prevShow" width="16" height="18" src="$sbRoot/images/prev.gif" alt="<<" title="Prev Show" /></div> +<select id="pickShow"> +#for $curShow in $sortedShowList: +<option value="$curShow.tvdbid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option> +#end for +</select> +<div class="navShow"><img id="nextShow" width="16" height="18" src="$sbRoot/images/next.gif" alt=">>" title="Next Show" /></div> +</div> + +<div id="summary" class="align-left"> +<table> +#if $show.network and $show.airs: + <tr><td class="showLegend">Airs: </td><td>$show.airs on $show.network</td></tr> +#else if $show.network: + <tr><td class="showLegend">Airs: </td><td>$show.network</td></tr> +#else if $show.airs: + <tr><td class="showLegend">Airs: </td><td>$show.airs</td></tr> +#end if + <tr><td class="showLegend">Status: </td><td>$show.status</td></tr> +#if $showLoc[1]: + <tr><td class="showLegend">Location: </td><td>$showLoc[0]</td></tr> +#else: + <tr><td class="showLegend"><span style="color: red;">Location: </span></td><td><span style="color: red;">$showLoc[0]</span> (dir is missing)</td></tr> +#end if +#set $anyQualities, $bestQualities = $Quality.splitQuality(int($show.quality)) + <tr><td class="showLegend">Quality: </td><td> +#if $show.quality in $qualityPresets: +$qualityPresetStrings[$show.quality] +#else: +#if $anyQualities: +initially download: <b><%=", ".join([Quality.qualityStrings[x] for x in anyQualities])%></b> #if $bestQualities then " + " else ""# +#end if +#if $bestQualities: +replace with: <b><%=", ".join([Quality.qualityStrings[x] for x in bestQualities])%></b> +#end if +#end if + </td></tr> + <tr><td class="showLegend">Language:</td><td><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="" /> $show.lang</td></tr> + <tr> + <td class="showLegend">Audio languages:</td> + <td> + <img src="$sbRoot/images/flags/${show.audio_lang}.png" alt="$show.audio_lang" width="16" /> $show.audio_lang + </td> + </tr> + <tr><td class="showLegend">Custom Search Names:</td><td>$show.custom_search_names</td></tr> + <tr><td class="showLegend">Flatten Folders: </td><td><img src="$sbRoot/images/#if $show.flatten_folders == 1 or $sickbeard.NAMING_FORCE_FOLDERS then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + <tr><td class="showLegend">Paused: </td><td><img src="$sbRoot/images/#if int($show.paused) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + <tr><td class="showLegend">Air-by-Date: </td><td><img src="$sbRoot/images/#if int($show.air_by_date) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + <tr><td class="showLegend">French Episodes : </td><td>$lfr</td></tr> + <tr><td class="showLegend">English Episodes : </td><td>$leng</td></tr> + <tr><td class="showLegend">Unknown Episodes : </td><td>$lno</td></tr> + <tr><td class="showLegend">Missing Episodes : </td><td>$lmanq</td></tr> +</table> +</div> + +<div class="float-left"> +Change selected episodes to +<select id="statusSelect"> +#for $curStatus in [$WANTED, $SKIPPED, $ARCHIVED, $IGNORED] + $Quality.DOWNLOADED: +#if $curStatus == $DOWNLOADED: +#continue +#end if +<option value="$curStatus">$statusStrings[$curStatus]</option> +#end for +</select> +<input type="hidden" id="showID" value="$show.tvdbid" /> +<input type="button" class="btn" id="changeStatus" value="Go" /><br /> +<br /> +<br /> +</div> + +<div class="float-right"> +Change Audio of selected episodes to +<select id="audioSelect"> +#for $audio in ['fr','en','']: +<option value="$audio">$showLanguages[$audio]</option> +#end for +</select> +<input type="hidden" id="showID" value="$show.tvdbid" /> +<input type="button" class="btn" id="changeAudio" value="Go" /><br /> +<br /> +<br /> +</div> + +#set $curSeason = -1 + +<div class="float-right clearfix" id="checkboxControls"> + <div style="padding-bottom: 5px;"> + <label for="wanted" class="checkbox inline wanted"><input type="checkbox" id="wanted" checked="checked" /> Wanted: <b>$epCounts[$Overview.WANTED]</b></label> + <label for="qual" class="checkbox inline qual"><input type="checkbox" id="qual" checked="checked" /> Low Quality: <b>$epCounts[$Overview.QUAL]</b></label> + <label for="good" class="checkbox inline good"><input type="checkbox" id="good" checked="checked" /> Downloaded: <b>$epCounts[$Overview.GOOD]</b></label> + <label for="skipped" class="checkbox inline skipped"><input type="checkbox" id="skipped" checked="checked" /> Skipped: <b>$epCounts[$Overview.SKIPPED]</b></label> + </div> + <div class="pull-right"> + <button class="btn btn-mini seriesCheck"><a href="#" onclick="return false;">Select Filtered Episodes</a></button> + <button class="btn btn-mini clearAll"><a href="#" onclick="return false;">Clear All</a></button> + </div> +</div> + +<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> + +#for $epResult in $sqlResults: + + #if int($epResult["season"]) != $curSeason: + <tr><td colspan="9"><a name="season-$epResult["season"]"></a></td></tr> + <tr class="seasonheader" id="season-$epResult["season"]"> + <td colspan="9"> + <h2>#if int($epResult["season"]) == 0 then "Specials" else "Season "+str($epResult["season"])#</h2> + </td> + </tr> + <tr id="season-$epResult["season"]-cols"><th width="1%"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th><th>NFO</th><th>TBN</th><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th><th>Audio</th><th>Status</th><th>Search</th></tr> + #set $curSeason = int($epResult["season"]) + #end if + + #set $epStr = str($epResult["season"]) + "x" + str($epResult["episode"]) + #set $epLoc = $epResult["location"] + <tr class="$Overview.overviewStrings[$epCats[$epStr]] season-$curSeason"> + <td width="1%"> +#if int($epResult["status"]) != $UNAIRED + <input type="checkbox" class="epCheck" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" /> +#end if + </td> + <td align="center"><img src="$sbRoot/images/#if $epResult["hasnfo"] == 1 then "nfo.gif\" alt=\"Y" else "nfo-no.gif\" alt=\"N"#" width="23" height="11" /></td> + <td align="center"><img src="$sbRoot/images/#if $epResult["hastbn"] == 1 then "tbn.gif\" alt=\"Y" else "tbn-no.gif\" alt=\"N"#" width="23" height="11" /></td> + <td align="center">$epResult["episode"]</td> + <td> + $epResult["name"] + #if $epResult["description"] != "" and $epResult["description"] != None: + <img src="$sbRoot/images/info32.png" height="16" class="plotInfo" alt="" id="plot_info_$show.tvdbid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>" /> + #end if + </td> + <td align="center" class="nowrap">#if int($epResult["airdate"]) == 1 then "never" else $datetime.date.fromordinal(int($epResult["airdate"]))#</td> + <td> +#if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower()): +$epLoc[len($show._location)+1:] +#elif $epLoc and (not $epLoc.lower().startswith($show._location.lower()) or not $show._location): +$epLoc +#end if + + </td> + <td align="center" class="audio_langs_column"> + #if len($epResult["audio_langs"]) > 0: + #for $language in $epResult["audio_langs"].split("|"): + <img src="$sbRoot/images/flags/${language}.png" alt="$language" width="16" /> + #end for + #end if + </td> + <td class="status_column">$statusStrings[int($epResult["status"])]</td> + <td align="center"> + #if int($epResult["season"]) != 0: + <a class="epSearch" href="searchEpisode?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/search16.png" height="16" alt="search" title="Manual Search" /></a> + #end if + </td> + </tr> + +#end for +</table><br /> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/editShow.tmpl b/data/interfaces/default/editShow.tmpl index be816155fc1a9499982de9cba6e57fb20234d3f4..c13f627aeb58ed31932b2420c7f1ee1fb3c8fcbe 100644 --- a/data/interfaces/default/editShow.tmpl +++ b/data/interfaces/default/editShow.tmpl @@ -1,96 +1,95 @@ -#import sickbeard -#from sickbeard import common -#from sickbeard import exceptions -#set global $title="Edit " + $show.name -#set global $header=$show.name - -#set global $sbPath=".." - -#set global $topmenu="home" -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ - - \$.getJSON('$sbRoot/home/addShows/getTVDBLanguages', {}, function(data) { - var resultStr = ''; - - if (data.results.length == 0) { - flag = ' class="flag" style="background-image:url($sbRoot/images/flags/${show.lang}.png)"'; - resultStr = '<option value="$show.lang" selected="selected" + flag>$show.lang</option>'; - } else { - var current_lang_added = false; - \$.each(data.results, function(index, obj) { - - if (obj == "$show.lang") { - selected = ' selected="selected"'; - current_lang_added = true; - } - else { - selected = ''; - } - - flag = ' class="flag" style="background-image:url($sbRoot/images/flags/' + obj + '.png);"'; - resultStr += '<option value="' + obj + '"' + selected + flag + '>' + obj + '</option>'; - }); - if (!current_lang_added) - resultStr += '<option value="$show.lang" selected="selected">$show.lang</option>'; - - } - \$('#tvdbLangSelect').html(resultStr) - - }); - -}); -//--> -</script> - - -<form action="editShow" method="post"> -<input type="hidden" name="show" value="$show.tvdbid" /> -Location: <input type="text" name="location" id="location" value="$show._location" size="50" /><br /> -<br /> -Quality: -#set $qualities = $common.Quality.splitQuality(int($show.quality)) -#set global $anyQualities = $qualities[0] -#set global $bestQualities = $qualities[1] -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_qualityChooser.tmpl") -<br /> -Metadata Language: <select name="tvdbLang" id="tvdbLangSelect"></select><br /> -Note: This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> -<br /> -<br /> -Desired Audio language: <select name="audio_lang" id="showLangSelect"> -#for $k,$v in $common.showLanguages.iteritems(): - <option value="$k" - #if $show.audio_lang == $k: - selected - #end if - >$v</option> -#end for -</select> -<br /> -<br /> -Custom Search Names: <input type="text" name="custom_search_names" id="custom_search_names" value="$show.custom_search_names" size="50" /><br /> -<strong>Note:</strong> Custom names used to find show. Define some custom names if show can't be found. Custom names should be separated by ",". Keep empty to use default show name (based on metadata language)<br /> -<br /> -Flatten files (no folders): <input type="checkbox" name="flatten_folders" #if $show.flatten_folders == 1 and not $sickbeard.NAMING_FORCE_FOLDERS then "checked=\"checked\"" else ""# #if $sickbeard.NAMING_FORCE_FOLDERS then "disabled=\"disabled\"" else ""#/><br /><br /> -Paused: <input type="checkbox" name="paused" #if $show.paused == 1 then "checked=\"checked\"" else ""# /><br /><br /> - -Air by date: -<input type="checkbox" name="air_by_date" #if $show.air_by_date == 1 then "checked=\"checked\"" else ""# /><br /> -(check this if the show is released as Show.03.02.2010 rather than Show.S02E03) -<br /><br /> -<input class="btn" type="submit" value="Submit" /> -</form> - -<script type="text/javascript" charset="utf-8"> -<!-- - jQuery('#location').fileBrowser({ title: 'Select Show Location' }); -//--> -</script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import common +#from sickbeard import exceptions +#set global $title="Edit " + $show.name +#set global $header=$show.name + +#set global $sbPath=".." + +#set global $topmenu="home" +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + + \$.getJSON('$sbRoot/home/addShows/getTVDBLanguages', {}, function(data) { + var resultStr = ''; + + if (data.results.length == 0) { + flag = ' class="flag" style="background-image:url($sbRoot/images/flags/${show.lang}.png)"'; + resultStr = '<option value="$show.lang" selected="selected" + flag>$show.lang</option>'; + } else { + var current_lang_added = false; + \$.each(data.results, function(index, obj) { + + if (obj == "$show.lang") { + selected = ' selected="selected"'; + current_lang_added = true; + } + else { + selected = ''; + } + + flag = ' class="flag" style="background-image:url($sbRoot/images/flags/' + obj + '.png);"'; + resultStr += '<option value="' + obj + '"' + selected + flag + '>' + obj + '</option>'; + }); + if (!current_lang_added) + resultStr += '<option value="$show.lang" selected="selected">$show.lang</option>'; + + } + \$('#tvdbLangSelect').html(resultStr) + + }); + +}); +//--> +</script> + + +<form action="editShow" method="post"> +<input type="hidden" name="show" value="$show.tvdbid" /> +Location: <input type="text" name="location" id="location" value="$show._location" size="50" /><br /> +<br /> +Quality: +#set $qualities = $common.Quality.splitQuality(int($show.quality)) +#set global $anyQualities = $qualities[0] +#set global $bestQualities = $qualities[1] +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_qualityChooser.tmpl") +Metadata Language: <select name="tvdbLang" id="tvdbLangSelect"></select><br /> +Note: This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> +<br /> +<br /> +Desired Audio language: <select name="audio_lang" id="showLangSelect"> +#for $k,$v in $common.showLanguages.iteritems(): + <option value="$k" + #if $show.audio_lang == $k: + selected + #end if + >$v</option> +#end for +</select> +<br /> +<br /> +Custom Search Names: <input type="text" name="custom_search_names" id="custom_search_names" value="$show.custom_search_names" size="50" /><br /> +<strong>Note:</strong> Custom names used to find show. Define some custom names if show can't be found. Custom names should be separated by ",". Keep empty to use default show name (based on metadata language)<br /> +<br /> +Flatten files (no folders): <input type="checkbox" name="flatten_folders" #if $show.flatten_folders == 1 and not $sickbeard.NAMING_FORCE_FOLDERS then "checked=\"checked\"" else ""# #if $sickbeard.NAMING_FORCE_FOLDERS then "disabled=\"disabled\"" else ""#/><br /><br /> +Paused: <input type="checkbox" name="paused" #if $show.paused == 1 then "checked=\"checked\"" else ""# /><br /><br /> + +Air by date: +<input type="checkbox" name="air_by_date" #if $show.air_by_date == 1 then "checked=\"checked\"" else ""# /><br /> +(check this if the show is released as Show.03.02.2010 rather than Show.S02E03) +<br /><br /> +<input class="btn" type="submit" value="Submit" /> +</form> + +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#location').fileBrowser({ title: 'Select Show Location' }); +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home.tmpl b/data/interfaces/default/home.tmpl index b6868d36967e588af557e38a12141a5d3122933a..f2c425a2d3b1d887d35d30a68e62f31027eb6d22 100644 --- a/data/interfaces/default/home.tmpl +++ b/data/interfaces/default/home.tmpl @@ -1,183 +1,216 @@ -#import sickbeard -#import datetime -#from sickbeard.common import * -#from sickbeard import db - -#set global $title="Home" -#set global $header="Show List" - -#set global $sbPath = ".." - -#set global $topmenu="home"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -#set $myDB = $db.DBConnection() -#set $today = str($datetime.date.today().toordinal()) -#set $downloadedEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE (status IN ("+",".join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]])+") OR (status IN ("+",".join([str(x) for x in $Quality.SNATCHED + $Quality.SNATCHED_PROPER])+") AND location != '')) AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") -#set $allEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE season != 0 and episode != 0 AND (airdate != 1 OR status IN ("+",".join([str(x) for x in ($Quality.DOWNLOADED + $Quality.SNATCHED + $Quality.SNATCHED_PROPER) + [$ARCHIVED]])+")) AND airdate <= "+$today+" AND status != "+str($IGNORED)+" GROUP BY showid") - -<script type="text/javascript" charset="utf-8"> -<!-- - -\$.tablesorter.addParser({ - id: 'loadingNames', - is: function(s) { - return false; - }, - format: function(s) { - if (s.indexOf('Loading...') == 0) - return s.replace('Loading...','000'); - return (s || '').replace(/^(The|A)\s/i,''); - }, - type: 'text' -}); - -\$.tablesorter.addParser({ - id: 'quality', - is: function(s) { - return false; - }, - format: function(s) { - return s.replace('hd',3).replace('sd',1).replace('any',0).replace('best',2).replace('custom',4); - }, - type: 'numeric' -}); - -\$.tablesorter.addParser({ - id: 'eps', - is: function(s) { - return false; - }, - format: function(s) { - match = s.match(/^(.*)/); - - if (match == null || match[1] == "?") - return -10; - - var nums = match[1].split(" / "); - - if (parseInt(nums[0]) === 0) - return parseInt(nums[1]); - - var finalNum = parseInt((nums[0]/nums[1])*1000)*100; - if (finalNum > 0) - finalNum += parseInt(nums[0]); - - return finalNum; - }, - type: 'numeric' -}); - -\$(document).ready(function(){ - - \$("#showListTable:has(tbody tr)").tablesorter({ - sortList: [[5,1],[1,0]], - textExtraction: { - 3: function(node) { return \$(node).find("span").text().toLowerCase(); }, - 4: function(node) { return \$(node).find("span").text(); }, - 5: function(node) { return \$(node).find("img").attr("alt"); } - }, - widgets: ['saveSort', 'zebra', 'stickyHeaders'], - headers: { - 0: { sorter: 'isoDate' }, - 1: { sorter: 'loadingNames' }, - 3: { sorter: 'quality' }, - 4: { sorter: 'eps' } - } - }); - -}); -//--> -</script> - -<table id="showListTable" class="tablesorter" cellspacing="1" border="0" cellpadding="0"> - - <thead><tr><th class="nowrap">Next Ep</th><th>Show</th><th>Network</th><th>Quality</th><th>Downloads</th><th>Active</th><th>Status</th></tr></thead> - <tfoot> - <tr> - <th rowspan="1" colspan="1" align="center"><a href="$sbRoot/home/addShows/">Add Show</a></th> - <th rowspan="1" colspan="6"></th> - </tr> - </tfoot> - <tbody> - -#for $curLoadingShow in $sickbeard.showQueueScheduler.action.loadingShowList: - - #if $curLoadingShow.show != None and $curLoadingShow.show in $sickbeard.showList: - #continue - #end if - - <tr> - <td align="center">(loading)</td> - <td> - #if $curLoadingShow.show == None: - Loading... ($curLoadingShow.show_name) - #else: - <a href="displayShow?show=$curLoadingShow.show.tvdbid">$curLoadingShow.show.name</a> - #end if - </td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - </tr> -#end for - -#set $myShowList = $list($sickbeard.showList) -$myShowList.sort(lambda x, y: cmp(x.name, y.name)) -#for $curShow in $myShowList: -#set $curEp = $curShow.nextEpisode() - -#set $curShowDownloads = [x[1] for x in $downloadedEps if int(x[0]) == $curShow.tvdbid] -#set $curShowAll = [x[1] for x in $allEps if int(x[0]) == $curShow.tvdbid] -#if len($curShowAll) != 0: - #if len($curShowDownloads) != 0: - #set $dlStat = str($curShowDownloads[0])+" / "+str($curShowAll[0]) - #set $nom = $curShowDownloads[0] - #set $den = $curShowAll[0] - #else - #set $dlStat = "0 / "+str($curShowAll[0]) - #set $nom = 0 - #set $den = $curShowAll[0] - #end if -#else - #set $dlStat = "?" - #set $nom = 0 - #set $den = 1 -#end if - - - <tr> - <td align="center" class="nowrap">#if len($curEp) != 0 then $curEp[0].airdate else ""#</td> - <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td> - <td>$curShow.network</td> -#if $curShow.quality in $qualityPresets: - <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td> -#else: - <td align="center"><span class="quality Custom">Custom</span></td> -#end if - <td align="center"><span style="display: none;">$dlStat</span><div id="progressbar$curShow.tvdbid" style="position:relative;"></div> - <script type="text/javascript"> - <!-- - \$(function() { - \$("\#progressbar$curShow.tvdbid").progressbar({ - value: parseInt($nom) * 100 / parseInt($den) - }); - \$("\#progressbar$curShow.tvdbid").append( "<div class='progressbarText'>$dlStat</div>" ) - }); +#import sickbeard +#import datetime +#from sickbeard.common import * +#from sickbeard import db + +#set global $title="Home" +#set global $header="Show List" + +#set global $sbPath = ".." + +#set global $topmenu="home"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +#set $myDB = $db.DBConnection() +#set $today = str($datetime.date.today().toordinal()) +#set $downloadedEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE (status IN ("+",".join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]])+") OR (status IN ("+",".join([str(x) for x in $Quality.SNATCHED + $Quality.SNATCHED_PROPER])+") AND location != '')) AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") +#set $allEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE season != 0 and episode != 0 AND (airdate != 1 OR status IN ("+",".join([str(x) for x in ($Quality.DOWNLOADED + $Quality.SNATCHED + $Quality.SNATCHED_PROPER) + [$ARCHIVED]])+")) AND airdate <= "+$today+" AND status != "+str($IGNORED)+" GROUP BY showid") +#set $fr = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'fr' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") + +<script type="text/javascript" charset="utf-8"> +<!-- + +\$.tablesorter.addParser({ + id: 'loadingNames', + is: function(s) { + return false; + }, + format: function(s) { + if (s.indexOf('Loading...') == 0) + return s.replace('Loading...','000'); + return (s || '').replace(/^(The|A)\s/i,''); + }, + type: 'text' +}); + +\$.tablesorter.addParser({ + id: 'quality', + is: function(s) { + return false; + }, + format: function(s) { + return s.replace('hd',3).replace('sd',1).replace('any',0).replace('best',2).replace('custom',4); + }, + type: 'numeric' +}); + +\$.tablesorter.addParser({ + id: 'eps', + is: function(s) { + return false; + }, + format: function(s) { + match = s.match(/^(.*)/); + + if (match == null || match[1] == "?") + return -10; + + var nums = match[1].split(" / "); + + if (parseInt(nums[0]) === 0) + return parseInt(nums[1]); + + var finalNum = parseInt((nums[0]/nums[1])*1000)*100; + if (finalNum > 0) + finalNum += parseInt(nums[0]); + + return finalNum; + }, + type: 'numeric' +}); + +\$(document).ready(function(){ + + \$("#showListTable:has(tbody tr)").tablesorter({ + sortList: [[6,1],[1,0]], + textExtraction: { + 3: function(node) { return \$(node).find("span").text().toLowerCase(); }, + 4: function(node) { return \$(node).find("span").text(); }, + 6: function(node) { return \$(node).find("img").attr("alt"); } + }, + widgets: ['saveSort', 'zebra', 'stickyHeaders'], + headers: { + 0: { sorter: 'isoDate' }, + 1: { sorter: 'loadingNames' }, + 3: { sorter: 'quality' }, + 4: { sorter: 'eps' }, + 5: { sorter: 'eps' } + } + }); + +}); +//--> +</script> + +<table id="showListTable" class="tablesorter" cellspacing="1" border="0" cellpadding="0"> + + <thead><tr><th class="nowrap">Next Ep</th><th>Show</th><th>Network</th><th>Quality</th><th>Downloads</th><th>French</th><th>Active</th><th>Audio</th><th>Status</th></tr></thead> + <tfoot> + <tr> + <th rowspan="1" colspan="1" align="center"><a href="$sbRoot/home/addShows/">Add Show</a></th> + <th rowspan="1" colspan="8"></th> + </tr> + </tfoot> + <tbody> + +#for $curLoadingShow in $sickbeard.showQueueScheduler.action.loadingShowList: + + #if $curLoadingShow.show != None and $curLoadingShow.show in $sickbeard.showList: + #continue + #end if + + <tr> + <td align="center">(loading)</td> + <td> + #if $curLoadingShow.show == None: + Loading... ($curLoadingShow.show_name) + #else: + <a href="displayShow?show=$curLoadingShow.show.tvdbid">$curLoadingShow.show.name</a> + #end if + </td> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + </tr> +#end for + +#set $myShowList = $list($sickbeard.showList) +$myShowList.sort(lambda x, y: cmp(x.name, y.name)) +#for $curShow in $myShowList: +#set $curEp = $curShow.nextEpisode() + +#set $curShowDownloads = [x[1] for x in $downloadedEps if int(x[0]) == $curShow.tvdbid] +#set $curfr = [x[1] for x in $fr if int(x[0]) == $curShow.tvdbid] +#set $curShowAll = [x[1] for x in $allEps if int(x[0]) == $curShow.tvdbid] +#if len($curShowAll) != 0: + #if len($curShowDownloads) != 0: + #set $dlStat = str($curShowDownloads[0])+" / "+str($curShowAll[0]) + #set $nom = $curShowDownloads[0] + #set $den = $curShowAll[0] + #else + #set $dlStat = "0 / "+str($curShowAll[0]) + #set $nom = 0 + #set $den = $curShowAll[0] + #end if +#else + #set $dlStat = "?" + #set $nom = 0 + #set $den = 1 +#end if +#if len($curShowDownloads) != 0: + #if len($curfr) != 0: + #set $frStat = str($curfr[0])+" / "+str($curShowDownloads[0]) + #set $nomfr = $curfr[0] + #set $denfr = $curShowDownloads[0] + #else + #set $frStat = "0 / "+str($curShowDownloads[0]) + #set $nomfr = 0 + #set $denfr = $curShowDownloads[0] + #end if +#else + #set $frStat = "0 / 0" + #set $nomfr = 0 + #set $denfr = 1 +#end if + + + <tr> + <td align="center" class="nowrap">#if len($curEp) != 0 then $curEp[0].airdate else ""#</td> + <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td> + <td>$curShow.network</td> +#if $curShow.quality in $qualityPresets: + <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td> +#else: + <td align="center"><span class="quality Custom">Custom</span></td> +#end if + <td align="center"><span style="display: none;">$dlStat</span><div id="progressbar$curShow.tvdbid" style="position:relative;"></div> + <script type="text/javascript"> + <!-- + \$(function() { + \$("\#progressbar$curShow.tvdbid").progressbar({ + value: parseInt($nom) * 100 / parseInt($den) + }); + \$("\#progressbar$curShow.tvdbid").append( "<div class='progressbarText'>$dlStat</div>" ) + }); //--> - </script> - </td> - <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 0 and $curShow.status != "Ended" then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> - <td align="center">$curShow.status</td> - </tr> - - -#end for -</tbody> -</table> - -<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") + </script> + </td> + <td align="center"><span style="display: none;">$frStat</span><div id="progressbar2$curShow.tvdbid" style="position:relative;"></div> + <script type="text/javascript"> + <!-- + \$(function() { + \$("\#progressbar2$curShow.tvdbid").progressbar({ + value: parseInt($nomfr) * 100 / parseInt($denfr) + }); + \$("\#progressbar2$curShow.tvdbid").append( "<div class='progressbarText'>$frStat</div>" ) + }); + //--> + </script> + </td> + <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 0 and $curShow.status != "Ended" then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> + <td align="center"> + <img src="$sbRoot/images/flags/${curShow.audio_lang}.png" alt="$curShow.audio_lang" width="16" /> + </td> + <td align="center">$curShow.status</td> + </tr> + + +#end for +</tbody> +</table> + +<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home_newShow.tmpl b/data/interfaces/default/home_newShow.tmpl index fd475604dafc273c265ad282e70036a0be990ff4..c53afebe13bacb3c9843d65e54fb444f5efe4d3f 100644 --- a/data/interfaces/default/home_newShow.tmpl +++ b/data/interfaces/default/home_newShow.tmpl @@ -1,85 +1,96 @@ -#import os.path -#import sickbeard -#set global $title="New Show" - -#set global $sbPath="../.." - -#set global $statpath="../.."# -#set global $topmenu="home"# -#import os.path - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/lib/formwizard.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/newShow.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script> - -<div id="displayText">aoeu</div> -<br /> - -<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8"> - -<fieldset class="sectionwrap"> - <legend class="legendStep">Find a show on the TVDB</legend> - - <div class="stepDiv"> - #if $use_provided_info: - Show retrieved from existing metadata: <a href="http://thetvdb.com/?tab=series&id=$provided_tvdb_id">$provided_tvdb_name</a> - <input type="hidden" name="tvdbLang" value="en" /> - <input type="hidden" name="whichSeries" value="$provided_tvdb_id" /> - <input type="hidden" id="providedName" value="$provided_tvdb_name" /> - #else: - <input type="text" id="nameToSearch" value="$default_show_name" /> - <select name="tvdbLang" id="tvdbLangSelect"> - <option value="en" selected="selected">en</option> - </select><b>*</b> - <input type="button" id="searchName" value="Search" class="btn" /><br /><br /> - - <b>*</b> This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> - This <b>DOES NOT</b> allow Sick Beard to download non-english TV episodes!<br /> - <br /> - <div id="searchResults" style="max-height: 225px; overflow: auto;"><br/></div> - #end if - </div> -</fieldset> - -<fieldset class="sectionwrap"> - <legend class="legendStep">Pick the parent folder</legend> - - <div class="stepDiv"> - #if $provided_show_dir: - Pre-chosen Destination Folder: <b>$provided_show_dir</b> <br /> - <input type="hidden" id="fullShowPath" name="fullShowPath" value="$provided_show_dir" /><br /> - #else - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_rootDirs.tmpl") - #end if - </div> -</fieldset> - -<fieldset class="sectionwrap"> - <legend class="legendStep">Customize options</legend> - - <div class="stepDiv"> - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_addShowOptions.tmpl") - </div> -</fieldset> - -#for $curNextDir in $other_shows: -<input type="hidden" name="other_shows" value="$curNextDir" /> -#end for -<input type="hidden" name="skipShow" id="skipShow" value="" /> -</form> - -<br /> - -<div style="width: 800px; text-align: center;"> -<input class="btn" type="button" id="addShowButton" value="Add Show" disabled="disabled" /> -#if $provided_show_dir: -<input class="btn" type="button" id="skipShowButton" value="Skip Show" /> -#end if -</div> - -<script type="text/javascript" src="$sbRoot/js/rootDirs.js?$sbPID"></script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import os.path +#import urllib +#import sickbeard +#set global $title="New Show" + +#set global $sbPath="../.." + +#set global $statpath="../.."# +#set global $topmenu="home"# +#import os.path +#from sickbeard import common + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" src="$sbRoot/js/lib/formwizard.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/newShow.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script> + +<div id="displayText">aoeu</div> +<br /> + +<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8"> + +<fieldset class="sectionwrap"> + <legend class="legendStep">Find a show on the TVDB</legend> + + <div class="stepDiv"> + #if $use_provided_info: + Show retrieved from existing metadata: <a href="http://thetvdb.com/?tab=series&id=$provided_tvdb_id">$provided_tvdb_name</a> + <input type="hidden" name="tvdbLang" value="en" /> + <input type="hidden" name="whichSeries" value="$provided_tvdb_id" /> + <input type="hidden" id="providedName" value="$provided_tvdb_name" /> + #else: + <input type="text" id="nameToSearch" value="$default_show_name" /> + <select name="tvdbLang" id="tvdbLangSelect"> + <option value="en" selected="selected">en</option> + </select><b>*</b> + Show language: + <select name="showLang" id="showLang"> + #for $k,$v in $common.showLanguages.iteritems(): + <option value="$k" + #if $k == 'en': + selected + #end if + >$v</option> + #end for + <input type="button" id="searchName" value="Search" class="btn" /><br /><br /> + + <b>*</b> This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> + This <b>DOES NOT</b> allow Sick Beard to download non-english TV episodes!<br /> + <br /> + <div id="searchResults" style="max-height: 225px; overflow: auto;"><br/></div> + #end if + </div> +</fieldset> + +<fieldset class="sectionwrap"> + <legend class="legendStep">Pick the parent folder</legend> + + <div class="stepDiv"> + #if $provided_show_dir: + Pre-chosen Destination Folder: <b>$provided_show_dir</b> <br /> + <input type="hidden" id="fullShowPath" name="fullShowPath" value="$provided_show_dir" /><br /> + #else + #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_rootDirs.tmpl") + #end if + </div> +</fieldset> + +<fieldset class="sectionwrap"> + <legend class="legendStep">Customize options</legend> + + <div class="stepDiv"> + #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_addShowOptions.tmpl") + </div> +</fieldset> + +#for $curNextDir in $other_shows: +<input type="hidden" name="other_shows" value="$curNextDir" /> +#end for +<input type="hidden" name="skipShow" id="skipShow" value="" /> +</form> + +<br /> + +<div style="width: 800px; text-align: center;"> +<input class="btn" type="button" id="addShowButton" value="Add Show" disabled="disabled" /> +#if $provided_show_dir: +<input class="btn" type="button" id="skipShowButton" value="Skip Show" /> +#end if +</div> + +<script type="text/javascript" src="$sbRoot/js/rootDirs.js?$sbPID"></script> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/js/displayShow.js b/data/js/displayShow.js index 11837b35764aa26653059fe4efd72ae7965cd32c..a1b435fee18aad46048c847fe5ef841cf9cb52a8 100644 --- a/data/js/displayShow.js +++ b/data/js/displayShow.js @@ -1,172 +1,194 @@ -$(document).ready(function(){ - - $('#sbRoot').ajaxEpSearch({'colorRow': true}); - - $('#seasonJump').change(function() { - var id = $(this).val(); - if (id && id != 'jump') { - $('html,body').animate({scrollTop: $(id).offset().top},'slow'); - location.hash = id; - } - $(this).val('jump'); - }); - - $("#prevShow").click(function(){ - var show = $('#pickShow option:selected'); - if (show.prev('option').length < 1){ - show.parent().children('option:last').attr('selected', 'selected'); - } else{ - show.prev('option').attr('selected', 'selected'); - }; - $("#pickShow").change(); - }); - - $("#nextShow").click(function(){ - var show = $('#pickShow option:selected'); - if (show.next('option').length < 1){ - show.parent().children('option:first').attr('selected', 'selected'); - } else{ - show.next('option').attr('selected', 'selected'); - }; - $("#pickShow").change(); - }); - - $('#changeStatus').click(function(){ - var sbRoot = $('#sbRoot').val() - var epArr = new Array() - - $('.epCheck').each(function() { - - if (this.checked == true) { - epArr.push($(this).attr('id')) - } - - }); - - if (epArr.length == 0) - return false - - url = sbRoot+'/home/setStatus?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&status='+$('#statusSelect').attr('value') - window.location.href = url - - }); - - $('.seasonCheck').click(function(){ - var seasCheck = this; - var seasNo = $(seasCheck).attr('id'); - - $('.epCheck:visible').each(function(){ - var epParts = $(this).attr('id').split('x') - - if (epParts[0] == seasNo) { - this.checked = seasCheck.checked - } - }); - }); - - var lastCheck = null; - $('.epCheck').click(function(event) { - - if(!lastCheck || !event.shiftKey) { - lastCheck = this; - return; - } - - var check = this; - var found = 0; - - $('.epCheck').each(function() { - switch (found) { - case 2: return false; - case 1: this.checked = lastCheck.checked; - } - - if (this == check || this == lastCheck) - found++; - }); - - lastClick = this; - }); - - // selects all visible episode checkboxes. - $('.seriesCheck').click(function(){ - $('.epCheck:visible').each(function(){ - this.checked = true - }); - $('.seasonCheck:visible').each(function(){ - this.checked = true - }) - }); - - // clears all visible episode checkboxes and the season selectors - $('.clearAll').click(function(){ - $('.epCheck:visible').each(function(){ - this.checked = false - }); - $('.seasonCheck:visible').each(function(){ - this.checked = false - }); - }); - - // handle the show selection dropbox - $('#pickShow').change(function(){ - var sbRoot = $('#sbRoot').val() - var val = $(this).attr('value') - if (val == 0) - return - url = sbRoot+'/home/displayShow?show='+val - window.location.href = url - }); - - // show/hide different types of rows when the checkboxes are changed - $("#checkboxControls input").change(function(e){ - var whichClass = $(this).attr('id') - $(this).showHideRows(whichClass) - return - $('tr.'+whichClass).each(function(i){ - $(this).toggle(); - }); - }); - - // initially show/hide all the rows according to the checkboxes - $("#checkboxControls input").each(function(e){ - var status = this.checked; - $("tr."+$(this).attr('id')).each(function(e){ - if (status) { - $(this).show(); - } else { - $(this).hide(); - } - }); - }); - - $.fn.showHideRows = function(whichClass){ - - var status = $('#checkboxControls > input, #'+whichClass).prop('checked') - $("tr."+whichClass).each(function(e){ - if (status) { - $(this).show(); - } else { - $(this).hide(); - } - }); - - // hide season headers with no episodes under them - $('tr.seasonheader').each(function(){ - var numRows = 0 - var seasonNo = $(this).attr('id') - $('tr.'+seasonNo+' :visible').each(function(){ - numRows++ - }) - if (numRows == 0) { - $(this).hide() - $('#'+seasonNo+'-cols').hide() - } else { - $(this).show() - $('#'+seasonNo+'-cols').show() - } - - }); - } - -}); +$(document).ready(function(){ + + $('#sbRoot').ajaxEpSearch({'colorRow': true}); + + $("td.status_column:contains('Snatched')").parent().css("background-color", "#EBC1EA"); + + $('#seasonJump').change(function() { + var id = $(this).val(); + if (id && id != 'jump') { + $('html,body').animate({scrollTop: $(id).offset().top},'slow'); + location.hash = id; + } + $(this).val('jump'); + }); + + $("#prevShow").click(function(){ + var show = $('#pickShow option:selected'); + if (show.prev('option').length < 1){ + show.parent().children('option:last').attr('selected', 'selected'); + } else{ + show.prev('option').attr('selected', 'selected'); + }; + $("#pickShow").change(); + }); + + $("#nextShow").click(function(){ + var show = $('#pickShow option:selected'); + if (show.next('option').length < 1){ + show.parent().children('option:first').attr('selected', 'selected'); + } else{ + show.next('option').attr('selected', 'selected'); + }; + $("#pickShow").change(); + }); + + $('#changeStatus').click(function(){ + var sbRoot = $('#sbRoot').val() + var epArr = new Array() + + $('.epCheck').each(function() { + + if (this.checked == true) { + epArr.push($(this).attr('id')) + } + + }); + + if (epArr.length == 0) + return false + + url = sbRoot+'/home/setStatus?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&status='+$('#statusSelect').attr('value') + window.location.href = url + + }); + + $('#changeAudio').click(function(){ + var sbRoot = $('#sbRoot').val() + var epArr = new Array() + + $('.epCheck').each(function() { + + if (this.checked == true) { + epArr.push($(this).attr('id')) + } + + }); + + if (epArr.length == 0) + return false + + url = sbRoot+'/home/setAudio?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&audio_langs='+$('#audioSelect').attr('value') + window.location.href = url + + }); + + $('.seasonCheck').click(function(){ + var seasCheck = this; + var seasNo = $(seasCheck).attr('id'); + + $('.epCheck:visible').each(function(){ + var epParts = $(this).attr('id').split('x') + + if (epParts[0] == seasNo) { + this.checked = seasCheck.checked + } + }); + }); + + var lastCheck = null; + $('.epCheck').click(function(event) { + + if(!lastCheck || !event.shiftKey) { + lastCheck = this; + return; + } + + var check = this; + var found = 0; + + $('.epCheck').each(function() { + switch (found) { + case 2: return false; + case 1: this.checked = lastCheck.checked; + } + + if (this == check || this == lastCheck) + found++; + }); + + lastClick = this; + }); + + // selects all visible episode checkboxes. + $('.seriesCheck').click(function(){ + $('.epCheck:visible').each(function(){ + this.checked = true + }); + $('.seasonCheck:visible').each(function(){ + this.checked = true + }) + }); + + // clears all visible episode checkboxes and the season selectors + $('.clearAll').click(function(){ + $('.epCheck:visible').each(function(){ + this.checked = false + }); + $('.seasonCheck:visible').each(function(){ + this.checked = false + }); + }); + + // handle the show selection dropbox + $('#pickShow').change(function(){ + var sbRoot = $('#sbRoot').val() + var val = $(this).attr('value') + if (val == 0) + return + url = sbRoot+'/home/displayShow?show='+val + window.location.href = url + }); + + // show/hide different types of rows when the checkboxes are changed + $("#checkboxControls input").change(function(e){ + var whichClass = $(this).attr('id') + $(this).showHideRows(whichClass) + return + $('tr.'+whichClass).each(function(i){ + $(this).toggle(); + }); + }); + + // initially show/hide all the rows according to the checkboxes + $("#checkboxControls input").each(function(e){ + var status = this.checked; + $("tr."+$(this).attr('id')).each(function(e){ + if (status) { + $(this).show(); + } else { + $(this).hide(); + } + }); + }); + + $.fn.showHideRows = function(whichClass){ + + var status = $('#checkboxControls > input, #'+whichClass).prop('checked') + $("tr."+whichClass).each(function(e){ + if (status) { + $(this).show(); + } else { + $(this).hide(); + } + }); + + // hide season headers with no episodes under them + $('tr.seasonheader').each(function(){ + var numRows = 0 + var seasonNo = $(this).attr('id') + $('tr.'+seasonNo+' :visible').each(function(){ + numRows++ + }) + if (numRows == 0) { + $(this).hide() + $('#'+seasonNo+'-cols').hide() + } else { + $(this).show() + $('#'+seasonNo+'-cols').show() + } + + }); + } + +}); diff --git a/lib/hachoir_core/iso639.py b/lib/hachoir_core/iso639.py index 61a0ba939bd4f7e3443a5f8008c7644e15f3ab23..b950b2cbcd181522b52ecfa4999b1395235b7862 100644 --- a/lib/hachoir_core/iso639.py +++ b/lib/hachoir_core/iso639.py @@ -554,5 +554,10 @@ ISO639_2 = {} for line in _ISO639: for key in line[1].split("/"): ISO639_2[key] = line[0] + ISO639_2 = {} +ISO639_1 = {} +for line in _ISO639: + if line[2]: + ISO639_1[line[2]] = line[0] del _ISO639 diff --git a/sickbeard/classes.py b/sickbeard/classes.py index 1001948d2b1d80846ce5ca5a8a2831c3f36564d2..1a640de04cb673d3488b66dd240ec2a6b0c27f6d 100644 --- a/sickbeard/classes.py +++ b/sickbeard/classes.py @@ -1,193 +1,196 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - - - -import sickbeard - -import urllib -import datetime - -from common import USER_AGENT, Quality - -class SickBeardURLopener(urllib.FancyURLopener): - version = USER_AGENT - -class AuthURLOpener(SickBeardURLopener): - """ - URLOpener class that supports http auth without needing interactive password entry. - If the provided username/password don't work it simply fails. - - user: username to use for HTTP auth - pw: password to use for HTTP auth - """ - def __init__(self, user, pw): - self.username = user - self.password = pw - - # remember if we've tried the username/password before - self.numTries = 0 - - # call the base class - urllib.FancyURLopener.__init__(self) - - def prompt_user_passwd(self, host, realm): - """ - Override this function and instead of prompting just give the - username/password that were provided when the class was instantiated. - """ - - # if this is the first try then provide a username/password - if self.numTries == 0: - self.numTries = 1 - return (self.username, self.password) - - # if we've tried before then return blank which cancels the request - else: - return ('', '') - - # this is pretty much just a hack for convenience - def openit(self, url): - self.numTries = 0 - return SickBeardURLopener.open(self, url) - -class SearchResult: - """ - Represents a search result from an indexer. - """ - - def __init__(self, episodes): - self.provider = -1 - - # URL to the NZB/torrent file - self.url = "" - - # used by some providers to store extra info associated with the result - self.extraInfo = [] - - # list of TVEpisode objects that this result is associated with - self.episodes = episodes - - # quality of the release - self.quality = Quality.UNKNOWN - - # release name - self.name = "" - - def __str__(self): - - if self.provider == None: - return "Invalid provider, unable to print self" - - myString = self.provider.name + " @ " + self.url + "\n" - myString += "Extra Info:\n" - for extra in self.extraInfo: - myString += " " + extra + "\n" - return myString - - def fileName(self): - return self.episodes[0].prettyName() + "." + self.resultType - -class NZBSearchResult(SearchResult): - """ - Regular NZB result with an URL to the NZB - """ - resultType = "nzb" - -class NZBDataSearchResult(SearchResult): - """ - NZB result where the actual NZB XML data is stored in the extraInfo - """ - resultType = "nzbdata" - -class TorrentDataSearchResult(SearchResult): - """ - Torrent result where the actual torrent data is stored in the extraInfo - """ - resultType = "torrentdata" - -class TorrentSearchResult(SearchResult): - """ - Torrent result with an URL to the torrent - """ - resultType = "torrent" - - -class ShowListUI: - """ - This class is for tvdb-api. Instead of prompting with a UI to pick the - desired result out of a list of shows it tries to be smart about it - based on what shows are in SB. - """ - def __init__(self, config, log=None): - self.config = config - self.log = log - - def selectSeries(self, allSeries): - idList = [x.tvdbid for x in sickbeard.showList] - - # try to pick a show that's in my show list - for curShow in allSeries: - if int(curShow['id']) in idList: - return curShow - - # if nothing matches then just go with the first match I guess - return allSeries[0] - -class Proper: - def __init__(self, name, url, date): - self.name = name - self.url = url - self.date = date - self.provider = None - self.quality = Quality.UNKNOWN - - self.tvdbid = -1 - self.season = -1 - self.episode = -1 - - def __str__(self): - return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid) - - -class ErrorViewer(): - """ - Keeps a static list of UIErrors to be displayed on the UI and allows - the list to be cleared. - """ - - errors = [] - - def __init__(self): - ErrorViewer.errors = [] - - @staticmethod - def add(error): - ErrorViewer.errors.append(error) - - @staticmethod - def clear(): - ErrorViewer.errors = [] - -class UIError(): - """ - Represents an error to be displayed in the web UI. - """ - def __init__(self, message): - self.message = message - self.time = datetime.datetime.now() +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + + + +import sickbeard + +import urllib +import datetime + +from common import USER_AGENT, Quality + +class SickBeardURLopener(urllib.FancyURLopener): + version = USER_AGENT + +class AuthURLOpener(SickBeardURLopener): + """ + URLOpener class that supports http auth without needing interactive password entry. + If the provided username/password don't work it simply fails. + + user: username to use for HTTP auth + pw: password to use for HTTP auth + """ + def __init__(self, user, pw): + self.username = user + self.password = pw + + # remember if we've tried the username/password before + self.numTries = 0 + + # call the base class + urllib.FancyURLopener.__init__(self) + + def prompt_user_passwd(self, host, realm): + """ + Override this function and instead of prompting just give the + username/password that were provided when the class was instantiated. + """ + + # if this is the first try then provide a username/password + if self.numTries == 0: + self.numTries = 1 + return (self.username, self.password) + + # if we've tried before then return blank which cancels the request + else: + return ('', '') + + # this is pretty much just a hack for convenience + def openit(self, url): + self.numTries = 0 + return SickBeardURLopener.open(self, url) + +class SearchResult: + """ + Represents a search result from an indexer. + """ + + def __init__(self, episodes): + self.provider = -1 + + # URL to the NZB/torrent file + self.url = "" + + # used by some providers to store extra info associated with the result + self.extraInfo = [] + + # list of TVEpisode objects that this result is associated with + self.episodes = episodes + + # quality of the release + self.quality = Quality.UNKNOWN + + # release name + self.name = "" + + # audio languages + self.audio_langs = [] + + def __str__(self): + + if self.provider == None: + return "Invalid provider, unable to print self" + + myString = self.provider.name + " @ " + self.url + "\n" + myString += "Extra Info:\n" + for extra in self.extraInfo: + myString += " " + extra + "\n" + return myString + + def fileName(self): + return self.episodes[0].prettyName() + "." + self.resultType + +class NZBSearchResult(SearchResult): + """ + Regular NZB result with an URL to the NZB + """ + resultType = "nzb" + +class NZBDataSearchResult(SearchResult): + """ + NZB result where the actual NZB XML data is stored in the extraInfo + """ + resultType = "nzbdata" + +class TorrentDataSearchResult(SearchResult): + """ + Torrent result where the actual torrent data is stored in the extraInfo + """ + resultType = "torrentdata" + +class TorrentSearchResult(SearchResult): + """ + Torrent result with an URL to the torrent + """ + resultType = "torrent" + + +class ShowListUI: + """ + This class is for tvdb-api. Instead of prompting with a UI to pick the + desired result out of a list of shows it tries to be smart about it + based on what shows are in SB. + """ + def __init__(self, config, log=None): + self.config = config + self.log = log + + def selectSeries(self, allSeries): + idList = [x.tvdbid for x in sickbeard.showList] + + # try to pick a show that's in my show list + for curShow in allSeries: + if int(curShow['id']) in idList: + return curShow + + # if nothing matches then just go with the first match I guess + return allSeries[0] + +class Proper: + def __init__(self, name, url, date): + self.name = name + self.url = url + self.date = date + self.provider = None + self.quality = Quality.UNKNOWN + + self.tvdbid = -1 + self.season = -1 + self.episode = -1 + + def __str__(self): + return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid) + + +class ErrorViewer(): + """ + Keeps a static list of UIErrors to be displayed on the UI and allows + the list to be cleared. + """ + + errors = [] + + def __init__(self): + ErrorViewer.errors = [] + + @staticmethod + def add(error): + ErrorViewer.errors.append(error) + + @staticmethod + def clear(): + ErrorViewer.errors = [] + +class UIError(): + """ + Represents an error to be displayed in the web UI. + """ + def __init__(self, message): + self.message = message + self.time = datetime.datetime.now() diff --git a/sickbeard/common.py b/sickbeard/common.py index 3da19925091fb20a17d1ec7a21519898fcf7ddc3..d96a60019981f1ed0ca3a02c312cdba5cddfb324 100644 --- a/sickbeard/common.py +++ b/sickbeard/common.py @@ -1,269 +1,281 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import os.path -import operator, platform -import re - -from sickbeard import version - -USER_AGENT = 'Sick Beard/alpha2-'+version.SICKBEARD_VERSION.replace(' ','-')+' ('+platform.system()+' '+platform.release()+')' - -mediaExtensions = ['avi', 'mkv', 'mpg', 'mpeg', 'wmv', - 'ogm', 'mp4', 'iso', 'img', 'divx', - 'm2ts', 'm4v', 'ts', 'flv', 'f4v', - 'mov', 'rmvb', 'vob', 'dvr-ms', 'wtv', - 'ogv', '3gp'] - -### Other constants -MULTI_EP_RESULT = -1 -SEASON_RESULT = -2 - -### Notification Types -NOTIFY_SNATCH = 1 -NOTIFY_DOWNLOAD = 2 - -notifyStrings = {} -notifyStrings[NOTIFY_SNATCH] = "Started Download" -notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished" - -### Episode statuses -UNKNOWN = -1 # should never happen -UNAIRED = 1 # episodes that haven't aired yet -SNATCHED = 2 # qualified with quality -WANTED = 3 # episodes we don't have but want to get -DOWNLOADED = 4 # qualified with quality -SKIPPED = 5 # episodes we don't want -ARCHIVED = 6 # episodes that you don't have locally (counts toward download completion stats) -IGNORED = 7 # episodes that you don't want included in your download stats -SNATCHED_PROPER = 9 # qualified with quality - -NAMING_REPEAT = 1 -NAMING_EXTEND = 2 -NAMING_DUPLICATE = 4 -NAMING_LIMITED_EXTEND = 8 -NAMING_SEPARATED_REPEAT = 16 -NAMING_LIMITED_EXTEND_E_PREFIXED = 32 - -multiEpStrings = {} -multiEpStrings[NAMING_REPEAT] = "Repeat" -multiEpStrings[NAMING_SEPARATED_REPEAT] = "Repeat (Separated)" -multiEpStrings[NAMING_DUPLICATE] = "Duplicate" -multiEpStrings[NAMING_EXTEND] = "Extend" -multiEpStrings[NAMING_LIMITED_EXTEND] = "Extend (Limited)" -multiEpStrings[NAMING_LIMITED_EXTEND_E_PREFIXED] = "Extend (Limited, E-prefixed)" - -class Quality: - - NONE = 0 - SDTV = 1 - SDDVD = 1<<1 # 2 - HDTV = 1<<2 # 4 - HDWEBDL = 1<<3 # 8 - HDBLURAY = 1<<4 # 16 - FULLHDBLURAY = 1<<5 # 32 - - # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere - UNKNOWN = 1<<15 - - qualityStrings = {NONE: "N/A", - UNKNOWN: "Unknown", - SDTV: "SD TV", - SDDVD: "SD DVD", - HDTV: "HD TV", - HDWEBDL: "720p WEB-DL", - HDBLURAY: "720p BluRay", - FULLHDBLURAY: "1080p BluRay"} - - statusPrefixes = {DOWNLOADED: "Downloaded", - SNATCHED: "Snatched"} - - @staticmethod - def _getStatusStrings(status): - toReturn = {} - for x in Quality.qualityStrings.keys(): - toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")" - return toReturn - - @staticmethod - def combineQualities(anyQualities, bestQualities): - anyQuality = 0 - bestQuality = 0 - if anyQualities: - anyQuality = reduce(operator.or_, anyQualities) - if bestQualities: - bestQuality = reduce(operator.or_, bestQualities) - return anyQuality | (bestQuality<<16) - - @staticmethod - def splitQuality(quality): - anyQualities = [] - bestQualities = [] - for curQual in Quality.qualityStrings.keys(): - if curQual & quality: - anyQualities.append(curQual) - if curQual<<16 & quality: - bestQualities.append(curQual) - - return (sorted(anyQualities), sorted(bestQualities)) - - @staticmethod - def nameQuality(name): - - name = os.path.basename(name) - - # if we have our exact text then assume we put it there - for x in Quality.qualityStrings: - if x == Quality.UNKNOWN: - continue - - regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W' - regex_match = re.search(regex, name, re.I) - if regex_match: - return x - - checkName = lambda list, func: func([re.search(x, name, re.I) for x in list]) - - if checkName(["(pdtv|hdtv|dsr|tvrip).(xvid|x264)"], all) and not checkName(["(720|1080)[pi]"], all): - return Quality.SDTV - elif checkName(["(dvdrip|bdrip)(.ws)?.(xvid|divx|x264)"], any) and not checkName(["(720|1080)[pi]"], all): - return Quality.SDDVD - elif checkName(["720p", "hdtv", "x264"], all) or checkName(["hr.ws.pdtv.x264"], any): - return Quality.HDTV - elif checkName(["720p", "web.dl"], all) or checkName(["720p", "itunes", "h.?264"], all): - return Quality.HDWEBDL - elif checkName(["720p", "bluray", "x264"], all) or checkName(["720p", "hddvd", "x264"], all): - return Quality.HDBLURAY - elif checkName(["1080p", "bluray", "x264"], all) or checkName(["1080p", "hddvd", "x264"], all): - return Quality.FULLHDBLURAY - else: - return Quality.UNKNOWN - - @staticmethod - def assumeQuality(name): - - if name.lower().endswith((".avi", ".mp4")): - return Quality.SDTV - elif name.lower().endswith(".mkv"): - return Quality.HDTV - else: - return Quality.UNKNOWN - - @staticmethod - def compositeStatus(status, quality): - return status + 100 * quality - - @staticmethod - def qualityDownloaded(status): - return (status - DOWNLOADED) / 100 - - @staticmethod - def splitCompositeStatus(status): - if status == UNKNOWN: - return (UNKNOWN, Quality.UNKNOWN) - - """Returns a tuple containing (status, quality)""" - for x in sorted(Quality.qualityStrings.keys(), reverse=True): - if status > x*100: - return (status-x*100, x) - - return (Quality.NONE, status) - - @staticmethod - def statusFromName(name, assume=True): - quality = Quality.nameQuality(name) - if assume and quality == Quality.UNKNOWN: - quality = Quality.assumeQuality(name) - return Quality.compositeStatus(DOWNLOADED, quality) - - DOWNLOADED = None - SNATCHED = None - SNATCHED_PROPER = None - -Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] -Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] -Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] - -HD = Quality.combineQualities([Quality.HDTV, Quality.HDWEBDL, Quality.HDBLURAY], []) -SD = Quality.combineQualities([Quality.SDTV, Quality.SDDVD], []) -ANY = Quality.combineQualities([Quality.SDTV, Quality.SDDVD, Quality.HDTV, Quality.HDWEBDL, Quality.HDBLURAY, Quality.UNKNOWN], []) -BEST = Quality.combineQualities([Quality.SDTV, Quality.HDTV, Quality.HDWEBDL], [Quality.HDTV]) - -qualityPresets = (SD, HD, ANY) -qualityPresetStrings = {SD: "SD", - HD: "HD", - ANY: "Any"} - -class StatusStrings: - def __init__(self): - self.statusStrings = {UNKNOWN: "Unknown", - UNAIRED: "Unaired", - SNATCHED: "Snatched", - DOWNLOADED: "Downloaded", - SKIPPED: "Skipped", - SNATCHED_PROPER: "Snatched (Proper)", - WANTED: "Wanted", - ARCHIVED: "Archived", - IGNORED: "Ignored"} - - def __getitem__(self, name): - if name in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER: - status, quality = Quality.splitCompositeStatus(name) - if quality == Quality.NONE: - return self.statusStrings[status] - else: - return self.statusStrings[status]+" ("+Quality.qualityStrings[quality]+")" - else: - return self.statusStrings[name] - - def has_key(self, name): - return name in self.statusStrings or name in Quality.DOWNLOADED or name in Quality.SNATCHED or name in Quality.SNATCHED_PROPER - -statusStrings = StatusStrings() - -class Overview: - UNAIRED = UNAIRED # 1 - QUAL = 2 - WANTED = WANTED # 3 - GOOD = 4 - SKIPPED = SKIPPED # 5 - - overviewStrings = {SKIPPED: "skipped", - WANTED: "wanted", - QUAL: "qual", - GOOD: "good", - UNAIRED: "unaired"} - -# Get our xml namespaces correct for lxml -XML_NSMAP = {'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsd': 'http://www.w3.org/2001/XMLSchema'} - - -countryList = {'Australia': 'AU', - 'Canada': 'CA', - 'USA': 'US' - } - -showLanguages = {'en':'english', - 'fr':'french', - '':'unknown' - } - -languageShortCode = {'english':'en', - 'french':'fr' +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import os.path +import operator, platform +import re + +from sickbeard import version + +USER_AGENT = 'Sick Beard/alpha2-'+version.SICKBEARD_VERSION.replace(' ','-')+' ('+platform.system()+' '+platform.release()+')' + +mediaExtensions = ['avi', 'mkv', 'mpg', 'mpeg', 'wmv', + 'ogm', 'mp4', 'iso', 'img', 'divx', + 'm2ts', 'm4v', 'ts', 'flv', 'f4v', + 'mov', 'rmvb', 'vob', 'dvr-ms', 'wtv', + 'ogv', '3gp'] + +### Other constants +MULTI_EP_RESULT = -1 +SEASON_RESULT = -2 + +### Notification Types +NOTIFY_SNATCH = 1 +NOTIFY_DOWNLOAD = 2 + +notifyStrings = {} +notifyStrings[NOTIFY_SNATCH] = "Started Download" +notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished" + +### Episode statuses +UNKNOWN = -1 # should never happen +UNAIRED = 1 # episodes that haven't aired yet +SNATCHED = 2 # qualified with quality +WANTED = 3 # episodes we don't have but want to get +DOWNLOADED = 4 # qualified with quality +SKIPPED = 5 # episodes we don't want +ARCHIVED = 6 # episodes that you don't have locally (counts toward download completion stats) +IGNORED = 7 # episodes that you don't want included in your download stats +SNATCHED_PROPER = 9 # qualified with quality +fr = 'fr' +NAMING_REPEAT = 1 +NAMING_EXTEND = 2 +NAMING_DUPLICATE = 4 +NAMING_LIMITED_EXTEND = 8 +NAMING_SEPARATED_REPEAT = 16 +NAMING_LIMITED_EXTEND_E_PREFIXED = 32 +en = 'en' +de = 'de' +unknown = '' +multiEpStrings = {} +multiEpStrings[NAMING_REPEAT] = "Repeat" +multiEpStrings[NAMING_SEPARATED_REPEAT] = "Repeat (Separated)" +multiEpStrings[NAMING_DUPLICATE] = "Duplicate" +multiEpStrings[NAMING_EXTEND] = "Extend" +multiEpStrings[NAMING_LIMITED_EXTEND] = "Extend (Limited)" +multiEpStrings[NAMING_LIMITED_EXTEND_E_PREFIXED] = "Extend (Limited, E-prefixed)" + +class Quality: + + NONE = 0 + SDTV = 1 + SDDVD = 1<<1 # 2 + HDTV = 1<<2 # 4 + HDWEBDL = 1<<3 # 8 + HDBLURAY = 1<<4 # 16 + FULLHDBLURAY = 1<<5 # 32 + + # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere + UNKNOWN = 1<<15 + + qualityStrings = {NONE: "N/A", + UNKNOWN: "Unknown", + SDTV: "SD TV", + SDDVD: "SD DVD", + HDTV: "HD TV", + HDWEBDL: "720p WEB-DL", + HDBLURAY: "720p BluRay", + FULLHDBLURAY: "1080p BluRay"} + + statusPrefixes = {DOWNLOADED: "Downloaded", + SNATCHED: "Snatched"} + + @staticmethod + def _getStatusStrings(status): + toReturn = {} + for x in Quality.qualityStrings.keys(): + toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")" + return toReturn + + @staticmethod + def combineQualities(anyQualities, bestQualities): + anyQuality = 0 + bestQuality = 0 + if anyQualities: + anyQuality = reduce(operator.or_, anyQualities) + if bestQualities: + bestQuality = reduce(operator.or_, bestQualities) + return anyQuality | (bestQuality<<16) + + @staticmethod + def splitQuality(quality): + anyQualities = [] + bestQualities = [] + for curQual in Quality.qualityStrings.keys(): + if curQual & quality: + anyQualities.append(curQual) + if curQual<<16 & quality: + bestQualities.append(curQual) + + return (sorted(anyQualities), sorted(bestQualities)) + + @staticmethod + def nameQuality(name): + + name = os.path.basename(name) + + # if we have our exact text then assume we put it there + for x in Quality.qualityStrings: + if x == Quality.UNKNOWN: + continue + + regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W' + regex_match = re.search(regex, name, re.I) + if regex_match: + return x + + checkName = lambda list, func: func([re.search(x, name, re.I) for x in list]) + + if checkName(["(pdtv|hdtv|dsr|tvrip).(xvid|x264)"], all) and not checkName(["(720|1080)[pi]"], all): + return Quality.SDTV + elif checkName(["(dvdrip|bdrip)(.ws)?.(xvid|divx|x264)"], any) and not checkName(["(720|1080)[pi]"], all): + return Quality.SDDVD + elif checkName(["720p", "hdtv", "x264"], all) or checkName(["hr.ws.pdtv.x264"], any): + return Quality.HDTV + elif checkName(["720p", "web.dl"], all) or checkName(["720p", "itunes", "h.?264"], all): + return Quality.HDWEBDL + elif checkName(["720p", "bluray", "x264"], all) or checkName(["720p", "hddvd", "x264"], all): + return Quality.HDBLURAY + elif checkName(["1080p", "bluray", "x264"], all) or checkName(["1080p", "hddvd", "x264"], all): + return Quality.FULLHDBLURAY + else: + return Quality.UNKNOWN + + @staticmethod + def assumeQuality(name): + + if name.lower().endswith((".avi", ".mp4")): + return Quality.SDTV + elif name.lower().endswith(".mkv"): + return Quality.HDTV + else: + return Quality.UNKNOWN + + @staticmethod + def compositeStatus(status, quality): + return status + 100 * quality + + @staticmethod + def qualityDownloaded(status): + return (status - DOWNLOADED) / 100 + + @staticmethod + def splitCompositeStatus(status): + if status == UNKNOWN: + return (UNKNOWN, Quality.UNKNOWN) + + """Returns a tuple containing (status, quality)""" + for x in sorted(Quality.qualityStrings.keys(), reverse=True): + if status > x*100: + return (status-x*100, x) + + return (Quality.NONE, status) + + @staticmethod + def statusFromName(name, assume=True): + quality = Quality.nameQuality(name) + if assume and quality == Quality.UNKNOWN: + quality = Quality.assumeQuality(name) + return Quality.compositeStatus(DOWNLOADED, quality) + + DOWNLOADED = None + SNATCHED = None + SNATCHED_PROPER = None + +Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] + +HD = Quality.combineQualities([Quality.HDTV, Quality.HDWEBDL, Quality.HDBLURAY], []) +SD = Quality.combineQualities([Quality.SDTV, Quality.SDDVD], []) +ANY = Quality.combineQualities([Quality.SDTV, Quality.SDDVD, Quality.HDTV, Quality.HDWEBDL, Quality.HDBLURAY, Quality.UNKNOWN], []) +BEST = Quality.combineQualities([Quality.SDTV, Quality.HDTV, Quality.HDWEBDL], [Quality.HDTV]) + +qualityPresets = (SD, HD, ANY) +qualityPresetStrings = {SD: "SD", + HD: "HD", + ANY: "Any"} + +class StatusStrings: + def __init__(self): + self.statusStrings = {UNKNOWN: "Unknown", + UNAIRED: "Unaired", + SNATCHED: "Snatched", + DOWNLOADED: "Downloaded", + SKIPPED: "Skipped", + SNATCHED_PROPER: "Snatched (Proper)", + WANTED: "Wanted", + ARCHIVED: "Archived", + IGNORED: "Ignored"} + + def __getitem__(self, name): + if name in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER: + status, quality = Quality.splitCompositeStatus(name) + if quality == Quality.NONE: + return self.statusStrings[status] + else: + return self.statusStrings[status]+" ("+Quality.qualityStrings[quality]+")" + else: + return self.statusStrings[name] + + def has_key(self, name): + return name in self.statusStrings or name in Quality.DOWNLOADED or name in Quality.SNATCHED or name in Quality.SNATCHED_PROPER + +statusStrings = StatusStrings() + +class Overview: + UNAIRED = UNAIRED # 1 + QUAL = 2 + WANTED = WANTED # 3 + GOOD = 4 + SKIPPED = SKIPPED # 5 + + overviewStrings = {SKIPPED: "skipped", + WANTED: "wanted", + QUAL: "qual", + GOOD: "good", + UNAIRED: "unaired"} + +class audio: + fr = 'fr' + en = 'en' + de = 'de' + unknown = '' + + audioStrings = {'fr': 'french', + 'en': 'english', + 'de': 'german', + '': 'unknown'} +# Get our xml namespaces correct for lxml +XML_NSMAP = {'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsd': 'http://www.w3.org/2001/XMLSchema'} + + +countryList = {'Australia': 'AU', + 'Canada': 'CA', + 'USA': 'US' + } + +showLanguages = {'en':'english', + 'fr':'french', + '':'unknown' + } + +languageShortCode = {'english':'en', + 'french':'fr' } \ No newline at end of file diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index 87b3eedf0aff9b2431cecf9b36efb725699d85ff..3cd88fd7a1d177462b65759c3ba0d4c18a7bf7e2 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -17,12 +17,13 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import sickbeard -import os.path +import shutil, time, os.path, sys from sickbeard import db, common, helpers, logger from sickbeard.providers.generic import GenericProvider from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from sickbeard.name_parser.parser import NameParser, InvalidNameException class MainSanityCheck(db.DBSanityCheck): diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index ce18c1b11dcdd308b329972aeebee25af735cdd6..1f346448bd1ab2306411ff8809845781ac91f125 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -1,700 +1,716 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import StringIO, zlib, gzip -import os -import stat -import urllib, urllib2 -import re, socket -import shutil -import traceback -import time, sys - -from httplib import BadStatusLine - -from xml.dom.minidom import Node - -import sickbeard - -from sickbeard.exceptions import MultipleShowObjectsException, ex -from sickbeard import logger, classes -from sickbeard.common import USER_AGENT, mediaExtensions, XML_NSMAP - -from sickbeard import db -from sickbeard import encodingKludge as ek - -from lib.tvdb_api import tvdb_api, tvdb_exceptions - -import xml.etree.cElementTree as etree - -urllib._urlopener = classes.SickBeardURLopener() - -def indentXML(elem, level=0): - ''' - Does our pretty printing, makes Matt very happy - ''' - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indentXML(elem, level+1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - # Strip out the newlines from text - if elem.text: - elem.text = elem.text.replace('\n', ' ') - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - -def replaceExtension(file, newExt): - ''' - >>> replaceExtension('foo.avi', 'mkv') - 'foo.mkv' - >>> replaceExtension('.vimrc', 'arglebargle') - '.vimrc' - >>> replaceExtension('a.b.c', 'd') - 'a.b.d' - >>> replaceExtension('', 'a') - '' - >>> replaceExtension('foo.bar', '') - 'foo.' - ''' - sepFile = file.rpartition(".") - if sepFile[0] == "": - return file - else: - return sepFile[0] + "." + newExt - -def isMediaFile (file): - # ignore samples - if re.search('(^|[\W_])sample\d*[\W_]', file): - return False - - # ignore MAC OS's retarded "resource fork" files - if file.startswith('._'): - return False - - sepFile = file.rpartition(".") - if sepFile[2].lower() in mediaExtensions: - return True - else: - return False - -def sanitizeFileName (name): - ''' - >>> sanitizeFileName('a/b/c') - 'a-b-c' - >>> sanitizeFileName('abc') - 'abc' - >>> sanitizeFileName('a"b') - 'ab' - >>> sanitizeFileName('.a.b..') - 'a.b' - ''' - - # remove bad chars from the filename - name = re.sub(r'[\\/\*]', '-', name) - name = re.sub(r'[:"<>|?]', '', name) - - # remove leading/trailing periods and spaces - name = name.strip(' .') - - return name - - -def getURL (url, headers=[]): - """ - Returns a byte-string retrieved from the url provider. - """ - - opener = urllib2.build_opener() - opener.addheaders = [('User-Agent', USER_AGENT), ('Accept-Encoding', 'gzip,deflate')] - for cur_header in headers: - opener.addheaders.append(cur_header) - - try: - usock = opener.open(url) - url = usock.geturl() - encoding = usock.info().get("Content-Encoding") - - if encoding in ('gzip', 'x-gzip', 'deflate'): - content = usock.read() - if encoding == 'deflate': - data = StringIO.StringIO(zlib.decompress(content)) - else: - data = gzip.GzipFile('', 'rb', 9, StringIO.StringIO(content)) - result = data.read() - - else: - result = usock.read() - - usock.close() - - except urllib2.HTTPError, e: - logger.log(u"HTTP error " + str(e.code) + " while loading URL " + url, logger.WARNING) - return None - except urllib2.URLError, e: - logger.log(u"URL error " + str(e.reason) + " while loading URL " + url, logger.WARNING) - return None - except BadStatusLine: - logger.log(u"BadStatusLine error while loading URL " + url, logger.WARNING) - return None - except socket.timeout: - logger.log(u"Timed out while loading URL " + url, logger.WARNING) - return None - except ValueError: - logger.log(u"Unknown error while loading URL " + url, logger.WARNING) - return None - except Exception: - logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING) - return None - - return result - -def findCertainShow (showList, tvdbid): - results = filter(lambda x: x.tvdbid == tvdbid, showList) - if len(results) == 0: - return None - elif len(results) > 1: - raise MultipleShowObjectsException() - else: - return results[0] - -def findCertainTVRageShow (showList, tvrid): - - if tvrid == 0: - return None - - results = filter(lambda x: x.tvrid == tvrid, showList) - - if len(results) == 0: - return None - elif len(results) > 1: - raise MultipleShowObjectsException() - else: - return results[0] - - -def makeDir (dir): - if not ek.ek(os.path.isdir, dir): - try: - ek.ek(os.makedirs, dir) - except OSError: - return False - return True - -def makeShowNFO(showID, showDir): - - logger.log(u"Making NFO for show "+str(showID)+" in dir "+showDir, logger.DEBUG) - - if not makeDir(showDir): - logger.log(u"Unable to create show dir, can't make NFO", logger.ERROR) - return False - - showObj = findCertainShow(sickbeard.showList, showID) - if not showObj: - logger.log(u"This should never have happened, post a bug about this!", logger.ERROR) - raise Exception("BAD STUFF HAPPENED") - - tvdb_lang = showObj.lang - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if tvdb_lang and not tvdb_lang == 'en': - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) - - try: - myShow = t[int(showID)] - except tvdb_exceptions.tvdb_shownotfound: - logger.log(u"Unable to find show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) - raise - - except tvdb_exceptions.tvdb_error: - logger.log(u"TVDB is down, can't use its data to add this show", logger.ERROR) - raise - - # check for title and id - try: - if myShow["seriesname"] == None or myShow["seriesname"] == "" or myShow["id"] == None or myShow["id"] == "": - logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) - - return False - except tvdb_exceptions.tvdb_attributenotfound: - logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) - - return False - - tvNode = buildNFOXML(myShow) - # Make it purdy - indentXML( tvNode ) - nfo = etree.ElementTree( tvNode ) - - logger.log(u"Writing NFO to "+os.path.join(showDir, "tvshow.nfo"), logger.DEBUG) - nfo_filename = os.path.join(showDir, "tvshow.nfo").encode('utf-8') - nfo_fh = open(nfo_filename, 'w') - nfo.write( nfo_fh, encoding="utf-8" ) - - return True - -def buildNFOXML(myShow): - ''' - Build an etree.Element of the root node of an NFO file with the - data from `myShow`, a TVDB show object. - - >>> from collections import defaultdict - >>> from xml.etree.cElementTree import tostring - >>> show = defaultdict(lambda: None, _actors=[]) - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title /><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' - >>> show['seriesname'] = 'Peaches' - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' - >>> show['contentrating'] = 'PG' - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre /><premiered /><studio /></tvshow>' - >>> show['genre'] = 'Fruit|Edibles' - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre>Fruit / Edibles</genre><premiered /><studio /></tvshow>' - ''' - tvNode = etree.Element( "tvshow" ) - for ns in XML_NSMAP.keys(): - tvNode.set(ns, XML_NSMAP[ns]) - - title = etree.SubElement( tvNode, "title" ) - if myShow["seriesname"] != None: - title.text = myShow["seriesname"] - - rating = etree.SubElement( tvNode, "rating" ) - if myShow["rating"] != None: - rating.text = myShow["rating"] - - plot = etree.SubElement( tvNode, "plot" ) - if myShow["overview"] != None: - plot.text = myShow["overview"] - - episodeguide = etree.SubElement( tvNode, "episodeguide" ) - episodeguideurl = etree.SubElement( episodeguide, "url" ) - if myShow["id"] != None: - showurl = sickbeard.TVDB_BASE_URL + '/series/' + myShow["id"] + '/all/en.zip' - episodeguideurl.text = showurl - - mpaa = etree.SubElement( tvNode, "mpaa" ) - if myShow["contentrating"] != None: - mpaa.text = myShow["contentrating"] - - tvdbid = etree.SubElement( tvNode, "id" ) - if myShow["id"] != None: - tvdbid.text = myShow["id"] - - genre = etree.SubElement( tvNode, "genre" ) - if myShow["genre"] != None: - genre.text = " / ".join([x for x in myShow["genre"].split('|') if x != '']) - - premiered = etree.SubElement( tvNode, "premiered" ) - if myShow["firstaired"] != None: - premiered.text = myShow["firstaired"] - - studio = etree.SubElement( tvNode, "studio" ) - if myShow["network"] != None: - studio.text = myShow["network"] - - for actor in myShow['_actors']: - - cur_actor = etree.SubElement( tvNode, "actor" ) - - cur_actor_name = etree.SubElement( cur_actor, "name" ) - cur_actor_name.text = actor['name'] - cur_actor_role = etree.SubElement( cur_actor, "role" ) - cur_actor_role_text = actor['role'] - - if cur_actor_role_text != None: - cur_actor_role.text = cur_actor_role_text - - cur_actor_thumb = etree.SubElement( cur_actor, "thumb" ) - cur_actor_thumb_text = actor['image'] - - if cur_actor_thumb_text != None: - cur_actor_thumb.text = cur_actor_thumb_text - - return tvNode - - -def searchDBForShow(regShowName): - - showNames = [re.sub('[. -]', ' ', regShowName)] - - myDB = db.DBConnection() - - yearRegex = "([^()]+?)\s*(\()?(\d{4})(?(2)\))$" - - for showName in showNames: - - sqlResults = myDB.select("SELECT * FROM tv_shows WHERE show_name LIKE ? OR tvr_name LIKE ?", [showName, showName]) - - if len(sqlResults) == 1: - return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) - - else: - - # if we didn't get exactly one result then try again with the year stripped off if possible - match = re.match(yearRegex, showName) - if match and match.group(1): - logger.log(u"Unable to match original name but trying to manually strip and specify show year", logger.DEBUG) - sqlResults = myDB.select("SELECT * FROM tv_shows WHERE (show_name LIKE ? OR tvr_name LIKE ?) AND startyear = ?", [match.group(1)+'%', match.group(1)+'%', match.group(3)]) - - if len(sqlResults) == 0: - logger.log(u"Unable to match a record in the DB for "+showName, logger.DEBUG) - continue - elif len(sqlResults) > 1: - logger.log(u"Multiple results for "+showName+" in the DB, unable to match show name", logger.DEBUG) - continue - else: - return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) - - - return None - -def sizeof_fmt(num): - ''' - >>> sizeof_fmt(2) - '2.0 bytes' - >>> sizeof_fmt(1024) - '1.0 KB' - >>> sizeof_fmt(2048) - '2.0 KB' - >>> sizeof_fmt(2**20) - '1.0 MB' - >>> sizeof_fmt(1234567) - '1.2 MB' - ''' - for x in ['bytes','KB','MB','GB','TB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - num /= 1024.0 - -def listMediaFiles(dir): - - if not dir or not ek.ek(os.path.isdir, dir): - return [] - - files = [] - for curFile in ek.ek(os.listdir, dir): - fullCurFile = ek.ek(os.path.join, dir, curFile) - - # if it's a dir do it recursively - if ek.ek(os.path.isdir, fullCurFile) and not curFile.startswith('.') and not curFile == 'Extras': - files += listMediaFiles(fullCurFile) - - elif isMediaFile(curFile): - files.append(fullCurFile) - - return files - -def copyFile(srcFile, destFile): - ek.ek(shutil.copyfile, srcFile, destFile) - try: - ek.ek(shutil.copymode, srcFile, destFile) - except OSError: - pass - -def moveFile(srcFile, destFile): - try: - ek.ek(os.rename, srcFile, destFile) - fixSetGroupID(destFile) - except OSError: - copyFile(srcFile, destFile) - ek.ek(os.unlink, srcFile) - -def make_dirs(path): - """ - Creates any folders that are missing and assigns them the permissions of their - parents - """ - - logger.log(u"Checking if the path " + path + " already exists", logger.DEBUG) - - if not ek.ek(os.path.isdir, path): - # Windows, create all missing folders - if os.name == 'nt' or os.name == 'ce': - try: - logger.log(u"Folder " + path + " didn't exist, creating it", logger.DEBUG) - ek.ek(os.makedirs, path) - except (OSError, IOError), e: - logger.log(u"Failed creating " + path + " : " + ex(e), logger.ERROR) - return False - - # not Windows, create all missing folders and set permissions - else: - sofar = '' - folder_list = path.split(os.path.sep) - - # look through each subfolder and make sure they all exist - for cur_folder in folder_list: - sofar += cur_folder + os.path.sep; - - # if it exists then just keep walking down the line - if ek.ek(os.path.isdir, sofar): - continue - - try: - logger.log(u"Folder " + sofar + " didn't exist, creating it", logger.DEBUG) - ek.ek(os.mkdir, sofar) - # use normpath to remove end separator, otherwise checks permissions against itself - chmodAsParent(ek.ek(os.path.normpath, sofar)) - except (OSError, IOError), e: - logger.log(u"Failed creating " + sofar + " : " + ex(e), logger.ERROR) - return False - - return True - - -def rename_ep_file(cur_path, new_path): - """ - Creates all folders needed to move a file to its new location, renames it, then cleans up any folders - left that are now empty. - - cur_path: The absolute path to the file you want to move/rename - new_path: The absolute path to the destination for the file WITHOUT THE EXTENSION - """ - - new_dest_dir, new_dest_name = os.path.split(new_path) #@UnusedVariable - cur_file_name, cur_file_ext = os.path.splitext(cur_path) #@UnusedVariable - - # put the extension on the incoming file - new_path += cur_file_ext - - make_dirs(os.path.dirname(new_path)) - - # move the file - try: - logger.log(u"Renaming file from " + cur_path + " to " + new_path) - ek.ek(os.rename, cur_path, new_path) - except (OSError, IOError), e: - logger.log(u"Failed renaming " + cur_path + " to " + new_path + ": " + ex(e), logger.ERROR) - return False - - # clean up any old folders that are empty - delete_empty_folders(ek.ek(os.path.dirname, cur_path)) - - return True - - -def delete_empty_folders(check_empty_dir, keep_dir=None): - """ - Walks backwards up the path and deletes any empty folders found. - - check_empty_dir: The path to clean (absolute path to a folder) - keep_dir: Clean until this path is reached - """ - - # treat check_empty_dir as empty when it only contains these items - ignore_items = [] - - logger.log(u"Trying to clean any empty folders under " + check_empty_dir) - - # as long as the folder exists and doesn't contain any files, delete it - while ek.ek(os.path.isdir, check_empty_dir) and check_empty_dir != keep_dir: - - check_files = ek.ek(os.listdir, check_empty_dir) - - if not check_files or (len(check_files) <= len(ignore_items) and all([check_file in ignore_items for check_file in check_files])): - # directory is empty or contains only ignore_items - try: - logger.log(u"Deleting empty folder: " + check_empty_dir) - # need shutil.rmtree when ignore_items is really implemented - ek.ek(os.rmdir, check_empty_dir) - except (WindowsError, OSError), e: - logger.log(u"Unable to delete " + check_empty_dir + ": " + repr(e) + " / " + str(e), logger.WARNING) - break - check_empty_dir = ek.ek(os.path.dirname, check_empty_dir) - else: - break - - -def chmodAsParent(childPath): - if os.name == 'nt' or os.name == 'ce': - return - - parentPath = ek.ek(os.path.dirname, childPath) - - if not parentPath: - logger.log(u"No parent path provided in "+childPath+", unable to get permissions from it", logger.DEBUG) - return - - parentMode = stat.S_IMODE(os.stat(parentPath)[stat.ST_MODE]) - - childPathStat = ek.ek(os.stat, childPath) - childPath_mode = stat.S_IMODE(childPathStat[stat.ST_MODE]) - - if ek.ek(os.path.isfile, childPath): - childMode = fileBitFilter(parentMode) - else: - childMode = parentMode - - if childPath_mode == childMode: - return - - childPath_owner = childPathStat.st_uid - user_id = os.geteuid() - - if user_id !=0 and user_id != childPath_owner: - logger.log(u"Not running as root or owner of "+childPath+", not trying to set permissions", logger.DEBUG) - return - - try: - ek.ek(os.chmod, childPath, childMode) - logger.log(u"Setting permissions for %s to %o as parent directory has %o" % (childPath, childMode, parentMode), logger.DEBUG) - except OSError: - logger.log(u"Failed to set permission for %s to %o" % (childPath, childMode), logger.ERROR) - -def fileBitFilter(mode): - for bit in [stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH, stat.S_ISUID, stat.S_ISGID]: - if mode & bit: - mode -= bit - - return mode - -def fixSetGroupID(childPath): - if os.name == 'nt' or os.name == 'ce': - return - - parentPath = ek.ek(os.path.dirname, childPath) - parentStat = os.stat(parentPath) - parentMode = stat.S_IMODE(parentStat[stat.ST_MODE]) - - if parentMode & stat.S_ISGID: - parentGID = parentStat[stat.ST_GID] - childStat = ek.ek(os.stat, childPath) - childGID = childStat[stat.ST_GID] - - if childGID == parentGID: - return - - childPath_owner = childStat.st_uid - user_id = os.geteuid() - - if user_id !=0 and user_id != childPath_owner: - logger.log(u"Not running as root or owner of "+childPath+", not trying to set the set-group-ID", logger.DEBUG) - return - - try: - ek.ek(os.chown, childPath, -1, parentGID) #@UndefinedVariable - only available on UNIX - logger.log(u"Respecting the set-group-ID bit on the parent directory for %s" % (childPath), logger.DEBUG) - except OSError: - logger.log(u"Failed to respect the set-group-ID bit on the parent directory for %s (setting group ID %i)" % (childPath, parentGID), logger.ERROR) - -def sanitizeSceneName (name, ezrss=False): - """ - Takes a show name and returns the "scenified" version of it. - - ezrss: If true the scenified version will follow EZRSS's cracksmoker rules as best as possible - - Returns: A string containing the scene version of the show name given. - """ - - if not ezrss: - bad_chars = u",:()'!?\u2019" - # ezrss leaves : and ! in their show names as far as I can tell - else: - bad_chars = u",()'?\u2019" - - # strip out any bad chars - for x in bad_chars: - name = name.replace(x, "") - - # tidy up stuff that doesn't belong in scene names - name = name.replace("- ", ".").replace(" ", ".").replace("&", "and").replace('/','.') - name = re.sub("\.\.*", ".", name) - - if name.endswith('.'): - name = name[:-1] - - return name - -def create_https_certificates(ssl_cert, ssl_key): - """ - Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' - """ - try: - from OpenSSL import crypto #@UnresolvedImport - from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial #@UnresolvedImport - except: - logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING) - return False - - # Create the CA Certificate - cakey = createKeyPair(TYPE_RSA, 1024) - careq = createCertRequest(cakey, CN='Certificate Authority') - cacert = createCertificate(careq, (careq, cakey), serial, (0, 60*60*24*365*10)) # ten years - - cname = 'SickBeard' - pkey = createKeyPair(TYPE_RSA, 1024) - req = createCertRequest(pkey, CN=cname) - cert = createCertificate(req, (cacert, cakey), serial, (0, 60*60*24*365*10)) # ten years - - # Save the key and certificate to disk - try: - open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - except: - logger.log(u"Error creating SSL key and certificate", logger.ERROR) - return False - - return True - -if __name__ == '__main__': - import doctest - doctest.testmod() - -def get_xml_text(node): - text = "" - for child_node in node.childNodes: - if child_node.nodeType in (Node.CDATA_SECTION_NODE, Node.TEXT_NODE): - text += child_node.data - return text.strip() - -def backupVersionedFile(oldFile, version): - numTries = 0 - - newFile = oldFile + '.' + 'v'+str(version) - - while not ek.ek(os.path.isfile, newFile): - if not ek.ek(os.path.isfile, oldFile): - break - - try: - logger.log(u"Attempting to back up "+oldFile+" before migration...") - shutil.copy(oldFile, newFile) - logger.log(u"Done backup, proceeding with migration.") - break - except Exception, e: - logger.log(u"Error while trying to back up "+oldFile+": "+ex(e)) - numTries += 1 - time.sleep(1) - logger.log(u"Trying again.") - - if numTries >= 10: - logger.log(u"Unable to back up "+oldFile+", please do it manually.") - sys.exit(1) +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import StringIO, zlib, gzip +import os +import stat +import urllib, urllib2 +import re, socket +import shutil +import traceback +import time, sys + +from httplib import BadStatusLine + +from xml.dom.minidom import Node + +import sickbeard + +from sickbeard.exceptions import MultipleShowObjectsException, ex +from sickbeard import logger, classes, common +from sickbeard.common import USER_AGENT, mediaExtensions, XML_NSMAP + +from sickbeard import db +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex + +from lib.tvdb_api import tvdb_api, tvdb_exceptions + +import xml.etree.cElementTree as etree +import datetime + +urllib._urlopener = classes.SickBeardURLopener() + +def indentXML(elem, level=0): + ''' + Does our pretty printing, makes Matt very happy + ''' + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indentXML(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + # Strip out the newlines from text + if elem.text: + elem.text = elem.text.replace('\n', ' ') + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +def replaceExtension(file, newExt): + ''' + >>> replaceExtension('foo.avi', 'mkv') + 'foo.mkv' + >>> replaceExtension('.vimrc', 'arglebargle') + '.vimrc' + >>> replaceExtension('a.b.c', 'd') + 'a.b.d' + >>> replaceExtension('', 'a') + '' + >>> replaceExtension('foo.bar', '') + 'foo.' + ''' + sepFile = file.rpartition(".") + if sepFile[0] == "": + return file + else: + return sepFile[0] + "." + newExt + +def isMediaFile (file): + # ignore samples + if re.search('(^|[\W_])sample\d*[\W_]', file): + return False + + # ignore MAC OS's retarded "resource fork" files + if file.startswith('._'): + return False + + sepFile = file.rpartition(".") + if sepFile[2].lower() in mediaExtensions: + return True + else: + return False + +def sanitizeFileName (name): + ''' + >>> sanitizeFileName('a/b/c') + 'a-b-c' + >>> sanitizeFileName('abc') + 'abc' + >>> sanitizeFileName('a"b') + 'ab' + >>> sanitizeFileName('.a.b..') + 'a.b' + ''' + + # remove bad chars from the filename + name = re.sub(r'[\\/\*]', '-', name) + name = re.sub(r'[:"<>|?]', '', name) + + # remove leading/trailing periods and spaces + name = name.strip(' .') + + return name + + +def getURL (url, headers=[]): + """ + Returns a byte-string retrieved from the url provider. + """ + + opener = urllib2.build_opener() + opener.addheaders = [('User-Agent', USER_AGENT), ('Accept-Encoding', 'gzip,deflate')] + for cur_header in headers: + opener.addheaders.append(cur_header) + + try: + usock = opener.open(url) + url = usock.geturl() + encoding = usock.info().get("Content-Encoding") + + if encoding in ('gzip', 'x-gzip', 'deflate'): + content = usock.read() + if encoding == 'deflate': + data = StringIO.StringIO(zlib.decompress(content)) + else: + data = gzip.GzipFile('', 'rb', 9, StringIO.StringIO(content)) + result = data.read() + + else: + result = usock.read() + + usock.close() + + except urllib2.HTTPError, e: + logger.log(u"HTTP error " + str(e.code) + " while loading URL " + url, logger.WARNING) + return None + except urllib2.URLError, e: + logger.log(u"URL error " + str(e.reason) + " while loading URL " + url, logger.WARNING) + return None + except BadStatusLine: + logger.log(u"BadStatusLine error while loading URL " + url, logger.WARNING) + return None + except socket.timeout: + logger.log(u"Timed out while loading URL " + url, logger.WARNING) + return None + except ValueError: + logger.log(u"Unknown error while loading URL " + url, logger.WARNING) + return None + except Exception: + logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING) + return None + + return result + +def findCertainShow (showList, tvdbid): + results = filter(lambda x: x.tvdbid == tvdbid, showList) + if len(results) == 0: + return None + elif len(results) > 1: + raise MultipleShowObjectsException() + else: + return results[0] + +def findCertainTVRageShow (showList, tvrid): + + if tvrid == 0: + return None + + results = filter(lambda x: x.tvrid == tvrid, showList) + + if len(results) == 0: + return None + elif len(results) > 1: + raise MultipleShowObjectsException() + else: + return results[0] + + +def makeDir (dir): + if not ek.ek(os.path.isdir, dir): + try: + ek.ek(os.makedirs, dir) + except OSError: + return False + return True + +def makeShowNFO(showID, showDir): + + logger.log(u"Making NFO for show "+str(showID)+" in dir "+showDir, logger.DEBUG) + + if not makeDir(showDir): + logger.log(u"Unable to create show dir, can't make NFO", logger.ERROR) + return False + + showObj = findCertainShow(sickbeard.showList, showID) + if not showObj: + logger.log(u"This should never have happened, post a bug about this!", logger.ERROR) + raise Exception("BAD STUFF HAPPENED") + + tvdb_lang = showObj.lang + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if tvdb_lang and not tvdb_lang == 'en': + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) + + try: + myShow = t[int(showID)] + except tvdb_exceptions.tvdb_shownotfound: + logger.log(u"Unable to find show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) + raise + + except tvdb_exceptions.tvdb_error: + logger.log(u"TVDB is down, can't use its data to add this show", logger.ERROR) + raise + + # check for title and id + try: + if myShow["seriesname"] == None or myShow["seriesname"] == "" or myShow["id"] == None or myShow["id"] == "": + logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) + + return False + except tvdb_exceptions.tvdb_attributenotfound: + logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) + + return False + + tvNode = buildNFOXML(myShow) + # Make it purdy + indentXML( tvNode ) + nfo = etree.ElementTree( tvNode ) + + logger.log(u"Writing NFO to "+os.path.join(showDir, "tvshow.nfo"), logger.DEBUG) + nfo_filename = os.path.join(showDir, "tvshow.nfo").encode('utf-8') + nfo_fh = open(nfo_filename, 'w') + nfo.write( nfo_fh, encoding="utf-8" ) + + return True + +def buildNFOXML(myShow): + ''' + Build an etree.Element of the root node of an NFO file with the + data from `myShow`, a TVDB show object. + + >>> from collections import defaultdict + >>> from xml.etree.cElementTree import tostring + >>> show = defaultdict(lambda: None, _actors=[]) + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title /><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' + >>> show['seriesname'] = 'Peaches' + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' + >>> show['contentrating'] = 'PG' + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre /><premiered /><studio /></tvshow>' + >>> show['genre'] = 'Fruit|Edibles' + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre>Fruit / Edibles</genre><premiered /><studio /></tvshow>' + ''' + tvNode = etree.Element( "tvshow" ) + for ns in XML_NSMAP.keys(): + tvNode.set(ns, XML_NSMAP[ns]) + + title = etree.SubElement( tvNode, "title" ) + if myShow["seriesname"] != None: + title.text = myShow["seriesname"] + + rating = etree.SubElement( tvNode, "rating" ) + if myShow["rating"] != None: + rating.text = myShow["rating"] + + plot = etree.SubElement( tvNode, "plot" ) + if myShow["overview"] != None: + plot.text = myShow["overview"] + + episodeguide = etree.SubElement( tvNode, "episodeguide" ) + episodeguideurl = etree.SubElement( episodeguide, "url" ) + if myShow["id"] != None: + showurl = sickbeard.TVDB_BASE_URL + '/series/' + myShow["id"] + '/all/en.zip' + episodeguideurl.text = showurl + + mpaa = etree.SubElement( tvNode, "mpaa" ) + if myShow["contentrating"] != None: + mpaa.text = myShow["contentrating"] + + tvdbid = etree.SubElement( tvNode, "id" ) + if myShow["id"] != None: + tvdbid.text = myShow["id"] + + genre = etree.SubElement( tvNode, "genre" ) + if myShow["genre"] != None: + genre.text = " / ".join([x for x in myShow["genre"].split('|') if x != '']) + + premiered = etree.SubElement( tvNode, "premiered" ) + if myShow["firstaired"] != None: + premiered.text = myShow["firstaired"] + + studio = etree.SubElement( tvNode, "studio" ) + if myShow["network"] != None: + studio.text = myShow["network"] + + for actor in myShow['_actors']: + + cur_actor = etree.SubElement( tvNode, "actor" ) + + cur_actor_name = etree.SubElement( cur_actor, "name" ) + cur_actor_name.text = actor['name'] + cur_actor_role = etree.SubElement( cur_actor, "role" ) + cur_actor_role_text = actor['role'] + + if cur_actor_role_text != None: + cur_actor_role.text = cur_actor_role_text + + cur_actor_thumb = etree.SubElement( cur_actor, "thumb" ) + cur_actor_thumb_text = actor['image'] + + if cur_actor_thumb_text != None: + cur_actor_thumb.text = cur_actor_thumb_text + + return tvNode + + +def searchDBForShow(regShowName): + + showNames = [re.sub('[. -]', ' ', regShowName)] + + myDB = db.DBConnection() + + yearRegex = "([^()]+?)\s*(\()?(\d{4})(?(2)\))$" + + for showName in showNames: + + sqlResults = myDB.select("SELECT * FROM tv_shows WHERE show_name LIKE ? OR tvr_name LIKE ?", [showName, showName]) + + if len(sqlResults) == 1: + return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) + + else: + + # if we didn't get exactly one result then try again with the year stripped off if possible + match = re.match(yearRegex, showName) + if match and match.group(1): + logger.log(u"Unable to match original name but trying to manually strip and specify show year", logger.DEBUG) + sqlResults = myDB.select("SELECT * FROM tv_shows WHERE (show_name LIKE ? OR tvr_name LIKE ?) AND startyear = ?", [match.group(1)+'%', match.group(1)+'%', match.group(3)]) + + if len(sqlResults) == 0: + logger.log(u"Unable to match a record in the DB for "+showName, logger.DEBUG) + continue + elif len(sqlResults) > 1: + logger.log(u"Multiple results for "+showName+" in the DB, unable to match show name", logger.DEBUG) + continue + else: + return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) + + + return None + +def sizeof_fmt(num): + ''' + >>> sizeof_fmt(2) + '2.0 bytes' + >>> sizeof_fmt(1024) + '1.0 KB' + >>> sizeof_fmt(2048) + '2.0 KB' + >>> sizeof_fmt(2**20) + '1.0 MB' + >>> sizeof_fmt(1234567) + '1.2 MB' + ''' + for x in ['bytes','KB','MB','GB','TB']: + if num < 1024.0: + return "%3.1f %s" % (num, x) + num /= 1024.0 + +def listMediaFiles(dir): + + if not dir or not ek.ek(os.path.isdir, dir): + return [] + + files = [] + for curFile in ek.ek(os.listdir, dir): + fullCurFile = ek.ek(os.path.join, dir, curFile) + + # if it's a dir do it recursively + if ek.ek(os.path.isdir, fullCurFile) and not curFile.startswith('.') and not curFile == 'Extras': + files += listMediaFiles(fullCurFile) + + elif isMediaFile(curFile): + files.append(fullCurFile) + + return files + +def copyFile(srcFile, destFile): + ek.ek(shutil.copyfile, srcFile, destFile) + try: + ek.ek(shutil.copymode, srcFile, destFile) + except OSError: + pass + +def moveFile(srcFile, destFile): + try: + ek.ek(os.rename, srcFile, destFile) + fixSetGroupID(destFile) + except OSError: + copyFile(srcFile, destFile) + ek.ek(os.unlink, srcFile) + +def make_dirs(path): + """ + Creates any folders that are missing and assigns them the permissions of their + parents + """ + + logger.log(u"Checking if the path " + path + " already exists", logger.DEBUG) + + if not ek.ek(os.path.isdir, path): + # Windows, create all missing folders + if os.name == 'nt' or os.name == 'ce': + try: + logger.log(u"Folder " + path + " didn't exist, creating it", logger.DEBUG) + ek.ek(os.makedirs, path) + except (OSError, IOError), e: + logger.log(u"Failed creating " + path + " : " + ex(e), logger.ERROR) + return False + + # not Windows, create all missing folders and set permissions + else: + sofar = '' + folder_list = path.split(os.path.sep) + + # look through each subfolder and make sure they all exist + for cur_folder in folder_list: + sofar += cur_folder + os.path.sep; + + # if it exists then just keep walking down the line + if ek.ek(os.path.isdir, sofar): + continue + + try: + logger.log(u"Folder " + sofar + " didn't exist, creating it", logger.DEBUG) + ek.ek(os.mkdir, sofar) + # use normpath to remove end separator, otherwise checks permissions against itself + chmodAsParent(ek.ek(os.path.normpath, sofar)) + except (OSError, IOError), e: + logger.log(u"Failed creating " + sofar + " : " + ex(e), logger.ERROR) + return False + + return True + + +def rename_ep_file(cur_path, new_path): + """ + Creates all folders needed to move a file to its new location, renames it, then cleans up any folders + left that are now empty. + + cur_path: The absolute path to the file you want to move/rename + new_path: The absolute path to the destination for the file WITHOUT THE EXTENSION + """ + + new_dest_dir, new_dest_name = os.path.split(new_path) #@UnusedVariable + cur_file_name, cur_file_ext = os.path.splitext(cur_path) #@UnusedVariable + + # put the extension on the incoming file + new_path += cur_file_ext + + make_dirs(os.path.dirname(new_path)) + + # move the file + try: + logger.log(u"Renaming file from " + cur_path + " to " + new_path) + ek.ek(os.rename, cur_path, new_path) + except (OSError, IOError), e: + logger.log(u"Failed renaming " + cur_path + " to " + new_path + ": " + ex(e), logger.ERROR) + return False + + # clean up any old folders that are empty + delete_empty_folders(ek.ek(os.path.dirname, cur_path)) + + return True + + +def delete_empty_folders(check_empty_dir, keep_dir=None): + """ + Walks backwards up the path and deletes any empty folders found. + + check_empty_dir: The path to clean (absolute path to a folder) + keep_dir: Clean until this path is reached + """ + + # treat check_empty_dir as empty when it only contains these items + ignore_items = [] + + logger.log(u"Trying to clean any empty folders under " + check_empty_dir) + + # as long as the folder exists and doesn't contain any files, delete it + while ek.ek(os.path.isdir, check_empty_dir) and check_empty_dir != keep_dir: + + check_files = ek.ek(os.listdir, check_empty_dir) + + if not check_files or (len(check_files) <= len(ignore_items) and all([check_file in ignore_items for check_file in check_files])): + # directory is empty or contains only ignore_items + try: + logger.log(u"Deleting empty folder: " + check_empty_dir) + # need shutil.rmtree when ignore_items is really implemented + ek.ek(os.rmdir, check_empty_dir) + except (WindowsError, OSError), e: + logger.log(u"Unable to delete " + check_empty_dir + ": " + repr(e) + " / " + str(e), logger.WARNING) + break + check_empty_dir = ek.ek(os.path.dirname, check_empty_dir) + else: + break + + +def chmodAsParent(childPath): + if os.name == 'nt' or os.name == 'ce': + return + + parentPath = ek.ek(os.path.dirname, childPath) + + if not parentPath: + logger.log(u"No parent path provided in "+childPath+", unable to get permissions from it", logger.DEBUG) + return + + parentMode = stat.S_IMODE(os.stat(parentPath)[stat.ST_MODE]) + + childPathStat = ek.ek(os.stat, childPath) + childPath_mode = stat.S_IMODE(childPathStat[stat.ST_MODE]) + + if ek.ek(os.path.isfile, childPath): + childMode = fileBitFilter(parentMode) + else: + childMode = parentMode + + if childPath_mode == childMode: + return + + childPath_owner = childPathStat.st_uid + user_id = os.geteuid() + + if user_id !=0 and user_id != childPath_owner: + logger.log(u"Not running as root or owner of "+childPath+", not trying to set permissions", logger.DEBUG) + return + + try: + ek.ek(os.chmod, childPath, childMode) + logger.log(u"Setting permissions for %s to %o as parent directory has %o" % (childPath, childMode, parentMode), logger.DEBUG) + except OSError: + logger.log(u"Failed to set permission for %s to %o" % (childPath, childMode), logger.ERROR) + +def fileBitFilter(mode): + for bit in [stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH, stat.S_ISUID, stat.S_ISGID]: + if mode & bit: + mode -= bit + + return mode + +def fixSetGroupID(childPath): + if os.name == 'nt' or os.name == 'ce': + return + + parentPath = ek.ek(os.path.dirname, childPath) + parentStat = os.stat(parentPath) + parentMode = stat.S_IMODE(parentStat[stat.ST_MODE]) + + if parentMode & stat.S_ISGID: + parentGID = parentStat[stat.ST_GID] + childStat = ek.ek(os.stat, childPath) + childGID = childStat[stat.ST_GID] + + if childGID == parentGID: + return + + childPath_owner = childStat.st_uid + user_id = os.geteuid() + + if user_id !=0 and user_id != childPath_owner: + logger.log(u"Not running as root or owner of "+childPath+", not trying to set the set-group-ID", logger.DEBUG) + return + + try: + ek.ek(os.chown, childPath, -1, parentGID) #@UndefinedVariable - only available on UNIX + logger.log(u"Respecting the set-group-ID bit on the parent directory for %s" % (childPath), logger.DEBUG) + except OSError: + logger.log(u"Failed to respect the set-group-ID bit on the parent directory for %s (setting group ID %i)" % (childPath, parentGID), logger.ERROR) + +def sanitizeSceneName (name, ezrss=False): + """ + Takes a show name and returns the "scenified" version of it. + + ezrss: If true the scenified version will follow EZRSS's cracksmoker rules as best as possible + + Returns: A string containing the scene version of the show name given. + """ + + if not ezrss: + bad_chars = u",:()'!?\u2019" + # ezrss leaves : and ! in their show names as far as I can tell + else: + bad_chars = u",()'?\u2019" + + # strip out any bad chars + for x in bad_chars: + name = name.replace(x, "") + + # tidy up stuff that doesn't belong in scene names + name = name.replace("- ", ".").replace(" ", ".").replace("&", "and").replace('/','.') + name = re.sub("\.\.*", ".", name) + + if name.endswith('.'): + name = name[:-1] + + return name + +def create_https_certificates(ssl_cert, ssl_key): + """ + Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' + """ + try: + from OpenSSL import crypto #@UnresolvedImport + from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial #@UnresolvedImport + except: + logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING) + return False + + # Create the CA Certificate + cakey = createKeyPair(TYPE_RSA, 1024) + careq = createCertRequest(cakey, CN='Certificate Authority') + cacert = createCertificate(careq, (careq, cakey), serial, (0, 60*60*24*365*10)) # ten years + + cname = 'SickBeard' + pkey = createKeyPair(TYPE_RSA, 1024) + req = createCertRequest(pkey, CN=cname) + cert = createCertificate(req, (cacert, cakey), serial, (0, 60*60*24*365*10)) # ten years + + # Save the key and certificate to disk + try: + open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + except: + logger.log(u"Error creating SSL key and certificate", logger.ERROR) + return False + + return True + +if __name__ == '__main__': + import doctest + doctest.testmod() + +def getAllLanguages (): + """ + Returns all show languages where an episode is wanted or unaired + + Returns: A list of all language codes + """ + myDB = db.DBConnection() + + sqlLanguages = myDB.select("SELECT DISTINCT(t.audio_lang) FROM tv_shows t, tv_episodes e WHERE t.tvdb_id = e.showid AND (e.status = ? OR e.status = ?)", [common.UNAIRED,common.WANTED]) + + languages = map(lambda x: str(x["audio_lang"]), sqlLanguages) + + return languages + +def get_xml_text(node): + text = "" + for child_node in node.childNodes: + if child_node.nodeType in (Node.CDATA_SECTION_NODE, Node.TEXT_NODE): + text += child_node.data + return text.strip() + +def backupVersionedFile(oldFile, version): + numTries = 0 + + newFile = oldFile + '.' + 'v'+str(version) + + while not ek.ek(os.path.isfile, newFile): + if not ek.ek(os.path.isfile, oldFile): + break + + try: + logger.log(u"Attempting to back up "+oldFile+" before migration...") + shutil.copy(oldFile, newFile) + logger.log(u"Done backup, proceeding with migration.") + break + except Exception, e: + logger.log(u"Error while trying to back up "+oldFile+": "+ex(e)) + numTries += 1 + time.sleep(1) + logger.log(u"Trying again.") + + if numTries >= 10: + logger.log(u"Unable to back up "+oldFile+", please do it manually.") + sys.exit(1) diff --git a/sickbeard/name_parser/parser.py b/sickbeard/name_parser/parser.py index d48746be61303b9120deaaf72fdc3412d44eda78..fe68ed36f7ad352986dd24ffa4ee140718df519e 100644 --- a/sickbeard/name_parser/parser.py +++ b/sickbeard/name_parser/parser.py @@ -1,355 +1,388 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import datetime -import os.path -import re - -import regexes - -import sickbeard - -from sickbeard import logger - -class NameParser(object): - def __init__(self, file_name=True): - - self.file_name = file_name - self.compiled_regexes = [] - self._compile_regexes() - - def clean_series_name(self, series_name): - """Cleans up series name by removing any . and _ - characters, along with any trailing hyphens. - - Is basically equivalent to replacing all _ and . with a - space, but handles decimal numbers in string, for example: - - >>> cleanRegexedSeriesName("an.example.1.0.test") - 'an example 1.0 test' - >>> cleanRegexedSeriesName("an_example_1.0_test") - 'an example 1.0 test' - - Stolen from dbr's tvnamer - """ - - series_name = re.sub("(\D)\.(?!\s)(\D)", "\\1 \\2", series_name) - series_name = re.sub("(\d)\.(\d{4})", "\\1 \\2", series_name) # if it ends in a year then don't keep the dot - series_name = re.sub("(\D)\.(?!\s)", "\\1 ", series_name) - series_name = re.sub("\.(?!\s)(\D)", " \\1", series_name) - series_name = series_name.replace("_", " ") - series_name = re.sub("-$", "", series_name) - return series_name.strip() - - def _compile_regexes(self): - for (cur_pattern_name, cur_pattern) in regexes.ep_regexes: - try: - cur_regex = re.compile(cur_pattern, re.VERBOSE | re.IGNORECASE) - except re.error, errormsg: - logger.log(u"WARNING: Invalid episode_pattern, %s. %s" % (errormsg, cur_pattern)) - else: - self.compiled_regexes.append((cur_pattern_name, cur_regex)) - - def _parse_string(self, name): - - if not name: - return None - - for (cur_regex_name, cur_regex) in self.compiled_regexes: - match = cur_regex.match(name) - - if not match: - continue - - result = ParseResult(name) - result.which_regex = [cur_regex_name] - - named_groups = match.groupdict().keys() - - if 'series_name' in named_groups: - result.series_name = match.group('series_name') - if result.series_name: - result.series_name = self.clean_series_name(result.series_name) - - if 'season_num' in named_groups: - tmp_season = int(match.group('season_num')) - if cur_regex_name == 'bare' and tmp_season in (19,20): - continue - result.season_number = tmp_season - - if 'ep_num' in named_groups: - ep_num = self._convert_number(match.group('ep_num')) - if 'extra_ep_num' in named_groups and match.group('extra_ep_num'): - result.episode_numbers = range(ep_num, self._convert_number(match.group('extra_ep_num'))+1) - else: - result.episode_numbers = [ep_num] - - if 'air_year' in named_groups and 'air_month' in named_groups and 'air_day' in named_groups: - year = int(match.group('air_year')) - month = int(match.group('air_month')) - day = int(match.group('air_day')) - - # make an attempt to detect YYYY-DD-MM formats - if month > 12: - tmp_month = month - month = day - day = tmp_month - - try: - result.air_date = datetime.date(year, month, day) - except ValueError, e: - raise InvalidNameException(e.message) - - if 'extra_info' in named_groups: - tmp_extra_info = match.group('extra_info') - - # Show.S04.Special is almost certainly not every episode in the season - if tmp_extra_info and cur_regex_name == 'season_only' and re.match(r'([. _-]|^)(special|extra)\w*([. _-]|$)', tmp_extra_info, re.I): - continue - result.extra_info = tmp_extra_info - - if 'release_group' in named_groups: - result.release_group = match.group('release_group') - - return result - - return None - - def _combine_results(self, first, second, attr): - # if the first doesn't exist then return the second or nothing - if not first: - if not second: - return None - else: - return getattr(second, attr) - - # if the second doesn't exist then return the first - if not second: - return getattr(first, attr) - - a = getattr(first, attr) - b = getattr(second, attr) - - # if a is good use it - if a != None or (type(a) == list and len(a)): - return a - # if not use b (if b isn't set it'll just be default) - else: - return b - - def _unicodify(self, obj, encoding = "utf-8"): - if isinstance(obj, basestring): - if not isinstance(obj, unicode): - obj = unicode(obj, encoding) - return obj - - def _convert_number(self, number): - if type(number) == int: - return number - - # good lord I'm lazy - if number.lower() == 'i': return 1 - if number.lower() == 'ii': return 2 - if number.lower() == 'iii': return 3 - if number.lower() == 'iv': return 4 - if number.lower() == 'v': return 5 - if number.lower() == 'vi': return 6 - if number.lower() == 'vii': return 7 - if number.lower() == 'viii': return 8 - if number.lower() == 'ix': return 9 - if number.lower() == 'x': return 10 - if number.lower() == 'xi': return 11 - if number.lower() == 'xii': return 12 - if number.lower() == 'xiii': return 13 - if number.lower() == 'xiv': return 14 - if number.lower() == 'xv': return 15 - if number.lower() == 'xvi': return 16 - if number.lower() == 'xvii': return 17 - if number.lower() == 'xviii': return 18 - if number.lower() == 'xix': return 19 - if number.lower() == 'xx': return 20 - if number.lower() == 'xxi': return 21 - if number.lower() == 'xxii': return 22 - if number.lower() == 'xxiii': return 23 - if number.lower() == 'xxiv': return 24 - if number.lower() == 'xxv': return 25 - if number.lower() == 'xxvi': return 26 - if number.lower() == 'xxvii': return 27 - if number.lower() == 'xxviii': return 28 - if number.lower() == 'xxix': return 29 - - return int(number) - - def parse(self, name): - - name = self._unicodify(name) - - cached = name_parser_cache.get(name) - if cached: - return cached - - # break it into parts if there are any (dirname, file name, extension) - dir_name, file_name = os.path.split(name) - ext_match = re.match('(.*)\.\w{3,4}$', file_name) - if ext_match and self.file_name: - base_file_name = ext_match.group(1) - else: - base_file_name = file_name - - # use only the direct parent dir - dir_name = os.path.basename(dir_name) - - # set up a result to use - final_result = ParseResult(name) - - # try parsing the file name - file_name_result = self._parse_string(base_file_name) - - # parse the dirname for extra info if needed - dir_name_result = self._parse_string(dir_name) - - # build the ParseResult object - final_result.air_date = self._combine_results(file_name_result, dir_name_result, 'air_date') - - if not final_result.air_date: - final_result.season_number = self._combine_results(file_name_result, dir_name_result, 'season_number') - final_result.episode_numbers = self._combine_results(file_name_result, dir_name_result, 'episode_numbers') - - # if the dirname has a release group/show name I believe it over the filename - final_result.series_name = self._combine_results(dir_name_result, file_name_result, 'series_name') - final_result.extra_info = self._combine_results(dir_name_result, file_name_result, 'extra_info') - final_result.release_group = self._combine_results(dir_name_result, file_name_result, 'release_group') - - final_result.which_regex = [] - if final_result == file_name_result: - final_result.which_regex = file_name_result.which_regex - elif final_result == dir_name_result: - final_result.which_regex = dir_name_result.which_regex - else: - if file_name_result: - final_result.which_regex += file_name_result.which_regex - if dir_name_result: - final_result.which_regex += dir_name_result.which_regex - - # if there's no useful info in it then raise an exception - if final_result.season_number == None and not final_result.episode_numbers and final_result.air_date == None and not final_result.series_name: - raise InvalidNameException("Unable to parse "+name.encode(sickbeard.SYS_ENCODING)) - - name_parser_cache.add(name, final_result) - # return it - return final_result - -class ParseResult(object): - def __init__(self, - original_name, - series_name=None, - season_number=None, - episode_numbers=None, - extra_info=None, - release_group=None, - air_date=None - ): - - self.original_name = original_name - - self.series_name = series_name - self.season_number = season_number - if not episode_numbers: - self.episode_numbers = [] - else: - self.episode_numbers = episode_numbers - - self.extra_info = extra_info - self.release_group = release_group - - self.air_date = air_date - - self.which_regex = None - - def __eq__(self, other): - if not other: - return False - - if self.series_name != other.series_name: - return False - if self.season_number != other.season_number: - return False - if self.episode_numbers != other.episode_numbers: - return False - if self.extra_info != other.extra_info: - return False - if self.release_group != other.release_group: - return False - if self.air_date != other.air_date: - return False - - return True - - def __str__(self): - if self.series_name != None: - to_return = self.series_name + u' - ' - else: - to_return = u'' - if self.season_number != None: - to_return += 'S'+str(self.season_number) - if self.episode_numbers and len(self.episode_numbers): - for e in self.episode_numbers: - to_return += 'E'+str(e) - - if self.air_by_date: - to_return += str(self.air_date) - - if self.extra_info: - to_return += ' - ' + self.extra_info - if self.release_group: - to_return += ' (' + self.release_group + ')' - - to_return += ' [ABD: '+str(self.air_by_date)+']' - - return to_return.encode('utf-8') - - def _is_air_by_date(self): - if self.season_number == None and len(self.episode_numbers) == 0 and self.air_date: - return True - return False - air_by_date = property(_is_air_by_date) - -class NameParserCache(object): - #TODO: check if the fifo list can beskiped and only use one dict - _previous_parsed_list = [] # keep a fifo list of the cached items - _previous_parsed = {} - _cache_size = 100 - - def add(self, name, parse_result): - self._previous_parsed[name] = parse_result - self._previous_parsed_list.append(name) - while len(self._previous_parsed_list) > self._cache_size: - del_me = self._previous_parsed_list.pop(0) - self._previous_parsed.pop(del_me) - - def get(self, name): - if name in self._previous_parsed: - logger.log("Using cached parse result for: " + name, logger.DEBUG) - return self._previous_parsed[name] - else: - return None - -name_parser_cache = NameParserCache() - -class InvalidNameException(Exception): - "The given name is not valid" +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import datetime +import os.path +import re + +import regexes + +import sickbeard + +from sickbeard import logger +from sickbeard.common import showLanguages + +class NameParser(object): + def __init__(self, file_name=True): + + self.file_name = file_name + self.compiled_regexes = [] + self.compiled_language_regexes =[] + self._compile_regexes() + + def clean_series_name(self, series_name): + """Cleans up series name by removing any . and _ + characters, along with any trailing hyphens. + + Is basically equivalent to replacing all _ and . with a + space, but handles decimal numbers in string, for example: + + >>> cleanRegexedSeriesName("an.example.1.0.test") + 'an example 1.0 test' + >>> cleanRegexedSeriesName("an_example_1.0_test") + 'an example 1.0 test' + + Stolen from dbr's tvnamer + """ + + series_name = re.sub("(\D)\.(?!\s)(\D)", "\\1 \\2", series_name) + series_name = re.sub("(\d)\.(\d{4})", "\\1 \\2", series_name) # if it ends in a year then don't keep the dot + series_name = re.sub("(\D)\.(?!\s)", "\\1 ", series_name) + series_name = re.sub("\.(?!\s)(\D)", " \\1", series_name) + series_name = series_name.replace("_", " ") + series_name = re.sub("-$", "", series_name) + return series_name.strip() + + def _compile_regexes(self): + for (cur_pattern_name, cur_pattern) in regexes.ep_regexes: + try: + cur_regex = re.compile(cur_pattern, re.VERBOSE | re.IGNORECASE) + except re.error, errormsg: + logger.log(u"WARNING: Invalid episode_pattern, %s. %s" % (errormsg, cur_pattern)) + else: + self.compiled_regexes.append((cur_pattern_name, cur_regex)) + + for (cur_pattern_name, cur_pattern) in regexes.language_regexes.iteritems(): + try: + cur_regex = re.compile(cur_pattern, re.VERBOSE | re.IGNORECASE) + except re.error, errormsg: + logger.log(u"WARNING: Invalid language_pattern, %s. %s" % (errormsg, cur_regex.pattern)) + else: + self.compiled_language_regexes.append((cur_pattern_name, cur_regex)) + + def _parse_string(self, name): + + if not name: + return None + + for (cur_regex_name, cur_regex) in self.compiled_regexes: + match = cur_regex.match(name) + + if not match: + continue + + result = ParseResult(name) + result.which_regex = [cur_regex_name] + + named_groups = match.groupdict().keys() + + if 'series_name' in named_groups: + result.series_name = match.group('series_name') + if result.series_name: + result.series_name = self.clean_series_name(result.series_name) + + if 'season_num' in named_groups: + tmp_season = int(match.group('season_num')) + if cur_regex_name == 'bare' and tmp_season in (19,20): + continue + result.season_number = tmp_season + + if 'ep_num' in named_groups: + ep_num = self._convert_number(match.group('ep_num')) + if 'extra_ep_num' in named_groups and match.group('extra_ep_num'): + result.episode_numbers = range(ep_num, self._convert_number(match.group('extra_ep_num'))+1) + else: + result.episode_numbers = [ep_num] + + if 'air_year' in named_groups and 'air_month' in named_groups and 'air_day' in named_groups: + year = int(match.group('air_year')) + month = int(match.group('air_month')) + day = int(match.group('air_day')) + + # make an attempt to detect YYYY-DD-MM formats + if month > 12: + tmp_month = month + month = day + day = tmp_month + + try: + result.air_date = datetime.date(year, month, day) + except ValueError, e: + raise InvalidNameException(e.message) + + if 'extra_info' in named_groups: + tmp_extra_info = match.group('extra_info') + + result.series_language = 'en' + + if tmp_extra_info: + for (cur_lang_regex_name, cur_lang_regex) in self.compiled_language_regexes: + lang_match = cur_lang_regex.match(name) + + if not lang_match: + continue + else: + logger.log(u"Found " + showLanguages.get(cur_lang_regex_name) + " episode",logger.DEBUG) + result.series_language = cur_lang_regex_name + + #if tmp_extra_info and re.search(r'(^|\w|[. _-])*(german)(([. _-])(dubbed))?\w*([. _-]|$)', tmp_extra_info, re.I): + # logger.log(u"Found german episode") + #result.series_language = 'de' + + + # Show.S04.Special is almost certainly not every episode in the season + if tmp_extra_info and cur_regex_name == 'season_only' and re.match(r'([. _-]|^)(special|extra)\w*([. _-]|$)', tmp_extra_info, re.I): + continue + result.extra_info = tmp_extra_info + + if 'release_group' in named_groups: + result.release_group = match.group('release_group') + + return result + + return None + + def _combine_results(self, first, second, attr): + # if the first doesn't exist then return the second or nothing + if not first: + if not second: + return None + else: + return getattr(second, attr) + + # if the second doesn't exist then return the first + if not second: + return getattr(first, attr) + + a = getattr(first, attr) + b = getattr(second, attr) + + # if a is good use it + if a != None or (type(a) == list and len(a)): + return a + # if not use b (if b isn't set it'll just be default) + else: + return b + + def _unicodify(self, obj, encoding = "utf-8"): + if isinstance(obj, basestring): + if not isinstance(obj, unicode): + obj = unicode(obj, encoding) + return obj + + def _convert_number(self, number): + if type(number) == int: + return number + + # good lord I'm lazy + if number.lower() == 'i': return 1 + if number.lower() == 'ii': return 2 + if number.lower() == 'iii': return 3 + if number.lower() == 'iv': return 4 + if number.lower() == 'v': return 5 + if number.lower() == 'vi': return 6 + if number.lower() == 'vii': return 7 + if number.lower() == 'viii': return 8 + if number.lower() == 'ix': return 9 + if number.lower() == 'x': return 10 + if number.lower() == 'xi': return 11 + if number.lower() == 'xii': return 12 + if number.lower() == 'xiii': return 13 + if number.lower() == 'xiv': return 14 + if number.lower() == 'xv': return 15 + if number.lower() == 'xvi': return 16 + if number.lower() == 'xvii': return 17 + if number.lower() == 'xviii': return 18 + if number.lower() == 'xix': return 19 + if number.lower() == 'xx': return 20 + if number.lower() == 'xxi': return 21 + if number.lower() == 'xxii': return 22 + if number.lower() == 'xxiii': return 23 + if number.lower() == 'xxiv': return 24 + if number.lower() == 'xxv': return 25 + if number.lower() == 'xxvi': return 26 + if number.lower() == 'xxvii': return 27 + if number.lower() == 'xxviii': return 28 + if number.lower() == 'xxix': return 29 + + return int(number) + + def parse(self, name): + + name = self._unicodify(name) + + cached = name_parser_cache.get(name) + if cached: + return cached + + # break it into parts if there are any (dirname, file name, extension) + dir_name, file_name = os.path.split(name) + ext_match = re.match('(.*)\.\w{3,4}$', file_name) + if ext_match and self.file_name: + base_file_name = ext_match.group(1) + else: + base_file_name = file_name + + # use only the direct parent dir + dir_name = os.path.basename(dir_name) + + # set up a result to use + final_result = ParseResult(name) + + # try parsing the file name + file_name_result = self._parse_string(base_file_name) + + # parse the dirname for extra info if needed + dir_name_result = self._parse_string(dir_name) + + # build the ParseResult object + final_result.air_date = self._combine_results(file_name_result, dir_name_result, 'air_date') + final_result.series_language = self._combine_results(file_name_result, dir_name_result, 'series_language') + + if not final_result.air_date: + final_result.season_number = self._combine_results(file_name_result, dir_name_result, 'season_number') + final_result.episode_numbers = self._combine_results(file_name_result, dir_name_result, 'episode_numbers') + + # if the dirname has a release group/show name I believe it over the filename + final_result.series_name = self._combine_results(dir_name_result, file_name_result, 'series_name') + final_result.extra_info = self._combine_results(dir_name_result, file_name_result, 'extra_info') + final_result.release_group = self._combine_results(dir_name_result, file_name_result, 'release_group') + + final_result.which_regex = [] + if final_result == file_name_result: + final_result.which_regex = file_name_result.which_regex + elif final_result == dir_name_result: + final_result.which_regex = dir_name_result.which_regex + else: + if file_name_result: + final_result.which_regex += file_name_result.which_regex + if dir_name_result: + final_result.which_regex += dir_name_result.which_regex + + # if there's no useful info in it then raise an exception + if final_result.season_number == None and not final_result.episode_numbers and final_result.air_date == None and not final_result.series_name: + raise InvalidNameException("Unable to parse "+name.encode(sickbeard.SYS_ENCODING)) + + name_parser_cache.add(name, final_result) + # return it + return final_result + +class ParseResult(object): + def __init__(self, + original_name, + series_name=None, + season_number=None, + episode_numbers=None, + extra_info=None, + release_group=None, + air_date=None, + series_language = 'en' + ): + + self.original_name = original_name + + self.series_name = series_name + self.season_number = season_number + if not episode_numbers: + self.episode_numbers = [] + else: + self.episode_numbers = episode_numbers + + self.extra_info = extra_info + self.release_group = release_group + + self.air_date = air_date + + self.which_regex = None + + self.series_language = series_language + + def __eq__(self, other): + if not other: + return False + + if self.series_name != other.series_name: + return False + if self.season_number != other.season_number: + return False + if self.episode_numbers != other.episode_numbers: + return False + if self.extra_info != other.extra_info: + return False + if self.release_group != other.release_group: + return False + if self.air_date != other.air_date: + return False + if self.series_language != other.series_language: + return False + + return True + + def __str__(self): + if self.series_name != None: + to_return = self.series_name + u' - ' + else: + to_return = u'' + if self.season_number != None: + to_return += 'S'+str(self.season_number) + if self.episode_numbers and len(self.episode_numbers): + for e in self.episode_numbers: + to_return += 'E'+str(e) + + if self.air_by_date: + to_return += str(self.air_date) + + if self.extra_info: + to_return += ' - ' + self.extra_info + if self.release_group: + to_return += ' (' + self.release_group + ')' + + to_return += ' [ABD: '+str(self.air_by_date)+']' + + return to_return.encode('utf-8') + + def _is_air_by_date(self): + if self.season_number == None and len(self.episode_numbers) == 0 and self.air_date: + return True + return False + air_by_date = property(_is_air_by_date) + +class NameParserCache(object): + #TODO: check if the fifo list can beskiped and only use one dict + _previous_parsed_list = [] # keep a fifo list of the cached items + _previous_parsed = {} + _cache_size = 100 + + def add(self, name, parse_result): + self._previous_parsed[name] = parse_result + self._previous_parsed_list.append(name) + while len(self._previous_parsed_list) > self._cache_size: + del_me = self._previous_parsed_list.pop(0) + self._previous_parsed.pop(del_me) + + def get(self, name): + if name in self._previous_parsed: + logger.log("Using cached parse result for: " + name, logger.DEBUG) + return self._previous_parsed[name] + else: + return None + +name_parser_cache = NameParserCache() + +class InvalidNameException(Exception): + "The given name is not valid" diff --git a/sickbeard/name_parser/regexes.py b/sickbeard/name_parser/regexes.py index 98a98aa3fd21741ab5d6e3c97bc5b71b11f76583..53620c3b7b2caf48e3e827f20ca007484a36497a 100644 --- a/sickbeard/name_parser/regexes.py +++ b/sickbeard/name_parser/regexes.py @@ -1,197 +1,204 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -# all regexes are case insensitive - -ep_regexes = [ - ('standard_repeat', - # Show.Name.S01E02.S01E03.Source.Quality.Etc-Group - # Show Name - S01E02 - S01E03 - S01E04 - Ep Name - ''' - ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator - s(?P<season_num>\d+)[. _-]* # S01 and optional separator - e(?P<ep_num>\d+) # E02 and separator - ([. _-]+s(?P=season_num)[. _-]* # S01 and optional separator - e(?P<extra_ep_num>\d+))+ # E03/etc and separator - [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - '''), - - ('fov_repeat', - # Show.Name.1x02.1x03.Source.Quality.Etc-Group - # Show Name - 1x02 - 1x03 - 1x04 - Ep Name - ''' - ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator - (?P<season_num>\d+)x # 1x - (?P<ep_num>\d+) # 02 and separator - ([. _-]+(?P=season_num)x # 1x - (?P<extra_ep_num>\d+))+ # 03/etc and separator - [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - '''), - - ('standard', - # Show.Name.S01E02.Source.Quality.Etc-Group - # Show Name - S01E02 - My Ep Name - # Show.Name.S01.E03.My.Ep.Name - # Show.Name.S01E02E03.Source.Quality.Etc-Group - # Show Name - S01E02-03 - My Ep Name - # Show.Name.S01.E02.E03 - ''' - ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator - s(?P<season_num>\d+)[. _-]* # S01 and optional separator - e(?P<ep_num>\d+) # E02 and separator - (([. _-]*e|-) # linking e/- char - (?P<extra_ep_num>(?!(1080|720)[pi])\d+))* # additional E03/etc - [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - '''), - - ('fov', - # Show_Name.1x02.Source_Quality_Etc-Group - # Show Name - 1x02 - My Ep Name - # Show_Name.1x02x03x04.Source_Quality_Etc-Group - # Show Name - 1x02-03-04 - My Ep Name - ''' - ^((?P<series_name>.+?)[\[. _-]+)? # Show_Name and separator - (?P<season_num>\d+)x # 1x - (?P<ep_num>\d+) # 02 and separator - (([. _-]*x|-) # linking x/- char - (?P<extra_ep_num> - (?!(1080|720)[pi])(?!(?<=x)264) # ignore obviously wrong multi-eps - \d+))* # additional x03/etc - [\]. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - '''), - - ('scene_date_format', - # Show.Name.2010.11.23.Source.Quality.Etc-Group - # Show Name - 2010-11-23 - Ep Name - ''' - ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator - (?P<air_year>\d{4})[. _-]+ # 2010 and separator - (?P<air_month>\d{2})[. _-]+ # 11 and separator - (?P<air_day>\d{2}) # 23 and separator - [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - '''), - - ('stupid', - # tpz-abc102 - ''' - (?P<release_group>.+?)-\w+?[\. ]? # tpz-abc - (?!264) # don't count x264 - (?P<season_num>\d{1,2}) # 1 - (?P<ep_num>\d{2})$ # 02 - '''), - - ('verbose', - # Show Name Season 1 Episode 2 Ep Name - ''' - ^(?P<series_name>.+?)[. _-]+ # Show Name and separator - season[. _-]+ # season and separator - (?P<season_num>\d+)[. _-]+ # 1 - episode[. _-]+ # episode and separator - (?P<ep_num>\d+)[. _-]+ # 02 and separator - (?P<extra_info>.+)$ # Source_Quality_Etc- - '''), - - ('season_only', - # Show.Name.S01.Source.Quality.Etc-Group - ''' - ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator - s(eason[. _-])? # S01/Season 01 - (?P<season_num>\d+)[. _-]* # S01 and optional separator - [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - ''' - ), - - ('no_season_multi_ep', - # Show.Name.E02-03 - # Show.Name.E02.2010 - ''' - ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator - (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part - (?P<ep_num>(\d+|[ivx]+)) # first ep num - ((([. _-]+(and|&|to)[. _-]+)|-) # and/&/to joiner - (?P<extra_ep_num>(?!(1080|720)[pi])(\d+|[ivx]+))[. _-]) # second ep num - ([. _-]*(?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - ''' - ), - - ('no_season_general', - # Show.Name.E23.Test - # Show.Name.Part.3.Source.Quality.Etc-Group - # Show.Name.Part.1.and.Part.2.Blah-Group - ''' - ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator - (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part - (?P<ep_num>(\d+|([ivx]+(?=[. _-])))) # first ep num - ([. _-]+((and|&|to)[. _-]+)? # and/&/to joiner - ((e(p(isode)?)?|part|pt)[. _-]?) # e, ep, episode, or part - (?P<extra_ep_num>(?!(1080|720)[pi]) - (\d+|([ivx]+(?=[. _-]))))[. _-])* # second ep num - ([. _-]*(?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - ''' - ), - - ('bare', - # Show.Name.102.Source.Quality.Etc-Group - ''' - ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator - (?P<season_num>\d{1,2}) # 1 - (?P<ep_num>\d{2}) # 02 and separator - ([. _-]+(?P<extra_info>(?!\d{3}[. _-]+)[^-]+) # Source_Quality_Etc- - (-(?P<release_group>.+))?)?$ # Group - '''), - - ('no_season', - # Show Name - 01 - Ep Name - # 01 - Ep Name - # 01 - Ep Name - ''' - ^((?P<series_name>.+?)(?:[. _-]{2,}|[. _]))? # Show_Name and separator - (?P<ep_num>\d{1,2}) # 02 - (?:-(?P<extra_ep_num>\d{1,2}))* # 02 - [. _-]+((?P<extra_info>.+?) # Source_Quality_Etc- - ((?<![. _-])(?<!WEB) # Make sure this is really the release group - -(?P<release_group>[^- ]+))?)?$ # Group - ''' - ), - - ('mm', - # engrenages S0311 HDTV Divx - ''' - ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator - s(?P<season_num>\d+)[. _-]* # S01 and optional separator - (?P<ep_num>\d+) # 02 and separator - ''' - ), - ] - +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +# all regexes are case insensitive +from sickbeard.common import showLanguages +ep_regexes = [ + ('standard_repeat', + # Show.Name.S01E02.S01E03.Source.Quality.Etc-Group + # Show Name - S01E02 - S01E03 - S01E04 - Ep Name + ''' + ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator + s(?P<season_num>\d+)[. _-]* # S01 and optional separator + e(?P<ep_num>\d+) # E02 and separator + ([. _-]+s(?P=season_num)[. _-]* # S01 and optional separator + e(?P<extra_ep_num>\d+))+ # E03/etc and separator + [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + '''), + + ('fov_repeat', + # Show.Name.1x02.1x03.Source.Quality.Etc-Group + # Show Name - 1x02 - 1x03 - 1x04 - Ep Name + ''' + ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator + (?P<season_num>\d+)x # 1x + (?P<ep_num>\d+) # 02 and separator + ([. _-]+(?P=season_num)x # 1x + (?P<extra_ep_num>\d+))+ # 03/etc and separator + [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + '''), + + ('standard', + # Show.Name.S01E02.Source.Quality.Etc-Group + # Show Name - S01E02 - My Ep Name + # Show.Name.S01.E03.My.Ep.Name + # Show.Name.S01E02E03.Source.Quality.Etc-Group + # Show Name - S01E02-03 - My Ep Name + # Show.Name.S01.E02.E03 + ''' + ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator + s(?P<season_num>\d+)[. _-]* # S01 and optional separator + e(?P<ep_num>\d+) # E02 and separator + (([. _-]*e|-) # linking e/- char + (?P<extra_ep_num>(?!(1080|720)[pi])\d+))* # additional E03/etc + [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + '''), + + ('fov', + # Show_Name.1x02.Source_Quality_Etc-Group + # Show Name - 1x02 - My Ep Name + # Show_Name.1x02x03x04.Source_Quality_Etc-Group + # Show Name - 1x02-03-04 - My Ep Name + ''' + ^((?P<series_name>.+?)[\[. _-]+)? # Show_Name and separator + (?P<season_num>\d+)x # 1x + (?P<ep_num>\d+) # 02 and separator + (([. _-]*x|-) # linking x/- char + (?P<extra_ep_num> + (?!(1080|720)[pi])(?!(?<=x)264) # ignore obviously wrong multi-eps + \d+))* # additional x03/etc + [\]. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + '''), + + ('scene_date_format', + # Show.Name.2010.11.23.Source.Quality.Etc-Group + # Show Name - 2010-11-23 - Ep Name + ''' + ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator + (?P<air_year>\d{4})[. _-]+ # 2010 and separator + (?P<air_month>\d{2})[. _-]+ # 11 and separator + (?P<air_day>\d{2}) # 23 and separator + [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + '''), + + ('stupid-mix', + # tpz-show102Source_Quality_Etc + ''' + [a-zA-Z0-9]{2,6}[. _-]+ # tpz-abc + (?P<series_name>.+?)[. _-]+ # Show Name and separator + (?!264) # don't count x264 + (?P<season_num>\d{1,2}) # 1 + (?P<ep_num>\d{2})[. _-]+ # 02 + (?P<extra_info>.+)$ # Source_Quality_Etc- + '''), + + ('stupid', + # tpz-abc102 + ''' + (?P<release_group>.+?)-\w+?[\. ]? # tpz-abc + (?!264) # don't count x264 + (?P<season_num>\d{1,2}) # 1 + (?P<ep_num>\d{2})$ # 02 + '''), + + ('verbose', + # Show Name Season 1 Episode 2 Ep Name + ''' + ^(?P<series_name>.+?)[. _-]+ # Show Name and separator + (sea|sai)son[. _-]+ # season and separator + (?P<season_num>\d+)[. _-]+ # 1 + episode[. _-]+ # episode and separator + (?P<ep_num>\d+)[. _-]+ # 02 and separator + (?P<extra_info>.+)$ # Source_Quality_Etc- + '''), + + ('season_only', + # Show.Name.S01.Source.Quality.Etc-Group + ''' + ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator + s(eason[. _-])? # S01/Season 01 + (?P<season_num>\d+)[. _-]* # S01 and optional separator + [. _-]*((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + ''' + ), + + ('no_season_multi_ep', + # Show.Name.E02-03 + # Show.Name.E02.2010 + ''' + ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator + (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part + (?P<ep_num>(\d+|[ivx]+)) # first ep num + ((([. _-]+(and|&|to)[. _-]+)|-) # and/&/to joiner + (?P<extra_ep_num>(?!(1080|720)[pi])(\d+|[ivx]+))[. _-]) # second ep num + ([. _-]*(?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + ''' + ), + + ('no_season_general', + # Show.Name.E23.Test + # Show.Name.Part.3.Source.Quality.Etc-Group + # Show.Name.Part.1.and.Part.2.Blah-Group + ''' + ^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator + (e(p(isode)?)?|part|pt)[. _-]? # e, ep, episode, or part + (?P<ep_num>(\d+|([ivx]+(?=[. _-])))) # first ep num + ([. _-]+((and|&|to)[. _-]+)? # and/&/to joiner + ((e(p(isode)?)?|part|pt)[. _-]?) # e, ep, episode, or part + (?P<extra_ep_num>(?!(1080|720)[pi]) + (\d+|([ivx]+(?=[. _-]))))[. _-])* # second ep num + ([. _-]*(?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + ''' + ), + + ('bare', + # Show.Name.102.Source.Quality.Etc-Group + ''' + ^(?P<series_name>.+?)[. _-]+ # Show_Name and separator + (?P<season_num>\d{1,2}) # 1 + (?P<ep_num>\d{2}) # 02 and separator + ([. _-]+(?P<extra_info>(?!\d{3}[. _-]+)[^-]+) # Source_Quality_Etc- + (-(?P<release_group>.+))?)?$ # Group + '''), + + ('no_season', + # Show Name - 01 - Ep Name + # 01 - Ep Name + # 01 - Ep Name + ''' + ^((?P<series_name>.+?)(?:[. _-]{2,}|[. _]))? # Show_Name and separator + (?P<ep_num>\d{1,2}) # 02 + (?:-(?P<extra_ep_num>\d{1,2}))? # 02 + [. _-]+((?P<extra_info>.+?) # Source_Quality_Etc- + ((?<![. _-])(?<!WEB) # Make sure this is really the release group + -(?P<release_group>[^- ]+))?)?$ # Group + ''' + ), + ] + +language_regexes = {} + +for k,v in showLanguages.iteritems(): + language_regexes[k] = '(^|\w|[. _-])*('+v+')(([. _-])(dubbed))?\w*([. _-]|$)' + diff --git a/sickbeard/providers/generic.py b/sickbeard/providers/generic.py index 37be3d16c854cdba4debc322318598dd1e2b0ea3..530959c8bc26dd8575d319cfe0642c86f1e8faf9 100644 --- a/sickbeard/providers/generic.py +++ b/sickbeard/providers/generic.py @@ -1,397 +1,415 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - - - -import datetime -import os -import sys -import re -import urllib2 - -import sickbeard - -from sickbeard import helpers, classes, logger, db - -from sickbeard.common import Quality, MULTI_EP_RESULT, SEASON_RESULT -from sickbeard import tvcache -from sickbeard import encodingKludge as ek -from sickbeard.exceptions import ex - -from lib.hachoir_parser import createParser - -from sickbeard.name_parser.parser import NameParser, InvalidNameException - -class GenericProvider: - - NZB = "nzb" - TORRENT = "torrent" - - def __init__(self, name): - - # these need to be set in the subclass - self.providerType = None - self.name = name - self.url = '' - - self.supportsBacklog = False - - self.cache = tvcache.TVCache(self) - - def getID(self): - return GenericProvider.makeID(self.name) - - @staticmethod - def makeID(name): - return re.sub("[^\w\d_]", "_", name).lower() - - def imageName(self): - return self.getID() + '.png' - - def _checkAuth(self): - return - - def isActive(self): - if self.providerType == GenericProvider.NZB and sickbeard.USE_NZBS: - return self.isEnabled() - elif self.providerType == GenericProvider.TORRENT and sickbeard.USE_TORRENTS: - return self.isEnabled() - else: - return False - - def isEnabled(self): - """ - This should be overridden and should return the config setting eg. sickbeard.MYPROVIDER - """ - return False - - def getResult(self, episodes): - """ - Returns a result of the correct type for this provider - """ - - if self.providerType == GenericProvider.NZB: - result = classes.NZBSearchResult(episodes) - elif self.providerType == GenericProvider.TORRENT: - result = classes.TorrentSearchResult(episodes) - else: - result = classes.SearchResult(episodes) - - result.provider = self - - return result - - - def getURL(self, url, headers=None): - """ - By default this is just a simple urlopen call but this method should be overridden - for providers with special URL requirements (like cookies) - """ - - if not headers: - headers = [] - - result = None - - result = helpers.getURL(url, headers) - - if result is None: - logger.log(u"Error loading "+self.name+" URL: " + url, logger.ERROR) - return None - - return result - - def downloadResult(self, result): - """ - Save the result to disk. - """ - - logger.log(u"Downloading a result from " + self.name+" at " + result.url) - - data = self.getURL(result.url) - - if data == None: - return False - - # use the appropriate watch folder - if self.providerType == GenericProvider.NZB: - saveDir = sickbeard.NZB_DIR - writeMode = 'w' - elif self.providerType == GenericProvider.TORRENT: - saveDir = sickbeard.TORRENT_DIR - writeMode = 'wb' - else: - return False - - # use the result name as the filename - fileName = ek.ek(os.path.join, saveDir, helpers.sanitizeFileName(result.name) + '.' + self.providerType) - - logger.log(u"Saving to " + fileName, logger.DEBUG) - - try: - fileOut = open(fileName, writeMode) - fileOut.write(data) - fileOut.close() - helpers.chmodAsParent(fileName) - except IOError, e: - logger.log("Unable to save the file: "+ex(e), logger.ERROR) - return False - - # as long as it's a valid download then consider it a successful snatch - return self._verify_download(fileName) - - def _verify_download(self, file_name=None): - """ - Checks the saved file to see if it was actually valid, if not then consider the download a failure. - """ - - # primitive verification of torrents, just make sure we didn't get a text file or something - if self.providerType == GenericProvider.TORRENT: - parser = createParser(file_name) - if parser: - mime_type = parser._getMimeType() - try: - parser.stream._input.close() - except: - pass - if mime_type != 'application/x-bittorrent': - logger.log(u"Result is not a valid torrent file", logger.WARNING) - return False - - return True - - def searchRSS(self): - self.cache.updateCache() - return self.cache.findNeededEpisodes() - - def getQuality(self, item): - """ - Figures out the quality of the given RSS item node - - item: An xml.dom.minidom.Node representing the <item> tag of the RSS feed - - Returns a Quality value obtained from the node's data - """ - (title, url) = self._get_title_and_url(item) #@UnusedVariable - quality = Quality.nameQuality(title) - return quality - - def _doSearch(self, show=None, season=None): - return [] - - def _get_season_search_strings(self, show, season, episode=None): - return [] - - def _get_episode_search_strings(self, ep_obj): - return [] - - def _get_title_and_url(self, item): - """ - Retrieves the title and URL data from the item XML node - - item: An xml.dom.minidom.Node representing the <item> tag of the RSS feed - - Returns: A tuple containing two strings representing title and URL respectively - """ - title = helpers.get_xml_text(item.getElementsByTagName('title')[0]) +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + + + +import datetime +import os +import sys +import re +import urllib2 + +import sickbeard + +from sickbeard import helpers, classes, logger, db + +from sickbeard.common import Quality, MULTI_EP_RESULT, SEASON_RESULT,\ + showLanguages +from sickbeard import tvcache +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex + +from lib.hachoir_parser import createParser + +from sickbeard.name_parser.parser import NameParser, InvalidNameException + +class GenericProvider: + + NZB = "nzb" + TORRENT = "torrent" + + def __init__(self, name): + + # these need to be set in the subclass + self.providerType = None + self.name = name + self.url = '' + + self.supportsBacklog = False + + self.cache = tvcache.TVCache(self) + + def getID(self): + return GenericProvider.makeID(self.name) + + @staticmethod + def makeID(name): + return re.sub("[^\w\d_]", "_", name).lower() + + def imageName(self): + return self.getID() + '.png' + + def _checkAuth(self): + return + + def isActive(self): + if self.providerType == GenericProvider.NZB and sickbeard.USE_NZBS: + return self.isEnabled() + elif self.providerType == GenericProvider.TORRENT and sickbeard.USE_TORRENTS: + return self.isEnabled() + else: + return False + + def isEnabled(self): + """ + This should be overridden and should return the config setting eg. sickbeard.MYPROVIDER + """ + return False + + def getResult(self, episodes): + """ + Returns a result of the correct type for this provider + """ + + if self.providerType == GenericProvider.NZB: + result = classes.NZBSearchResult(episodes) + elif self.providerType == GenericProvider.TORRENT: + result = classes.TorrentSearchResult(episodes) + else: + result = classes.SearchResult(episodes) + + result.provider = self + + return result + + + def getURL(self, url, headers=None): + """ + By default this is just a simple urlopen call but this method should be overridden + for providers with special URL requirements (like cookies) + """ + + if not headers: + headers = [] + + result = None + + result = helpers.getURL(url, headers) + + if result is None: + logger.log(u"Error loading "+self.name+" URL: " + url, logger.ERROR) + return None + + return result + + def downloadResult(self, result): + """ + Save the result to disk. + """ + + logger.log(u"Downloading a result from " + self.name+" at " + result.url) + + data = self.getURL(result.url) + + if data == None: + return False + + # use the appropriate watch folder + if self.providerType == GenericProvider.NZB: + saveDir = sickbeard.NZB_DIR + writeMode = 'w' + elif self.providerType == GenericProvider.TORRENT: + saveDir = sickbeard.TORRENT_DIR + writeMode = 'wb' + else: + return False + + # use the result name as the filename + fileName = ek.ek(os.path.join, saveDir, helpers.sanitizeFileName(result.name) + '.' + self.providerType) + + logger.log(u"Saving to " + fileName, logger.DEBUG) + try: - url = helpers.get_xml_text(item.getElementsByTagName('link')[0]) - if url: - url = url.replace('&','&') + fileOut = open(fileName, writeMode) + fileOut.write(data) + fileOut.close() + helpers.chmodAsParent(fileName) + except IOError, e: + logger.log("Unable to save the file: "+ex(e), logger.ERROR) + return False + + # as long as it's a valid download then consider it a successful snatch + return self._verify_download(fileName) + + def _verify_download(self, file_name=None): + """ + Checks the saved file to see if it was actually valid, if not then consider the download a failure. + """ + + # primitive verification of torrents, just make sure we didn't get a text file or something + if self.providerType == GenericProvider.TORRENT: + parser = createParser(file_name) + if parser: + mime_type = parser._getMimeType() + try: + parser.stream._input.close() + except: + pass + if mime_type != 'application/x-bittorrent': + logger.log(u"Result is not a valid torrent file", logger.WARNING) + return False + + return True + + def searchRSS(self): + self.cache.updateCache() + return self.cache.findNeededEpisodes() + + def getQuality(self, item): + """ + Figures out the quality of the given RSS item node + + item: An xml.dom.minidom.Node representing the <item> tag of the RSS feed + + Returns a Quality value obtained from the node's data + """ + (title, url) = self._get_title_and_url(item) #@UnusedVariable + quality = Quality.nameQuality(title) + return quality + + def _doSearch(self, show=None, season=None): + return [] + + def _get_season_search_strings(self, show, season, episode=None): + return [] + + def _get_episode_search_strings(self, ep_obj): + return [] + + def _get_title_and_url(self, item): + """ + Retrieves the title and URL data from the item XML node + + item: An xml.dom.minidom.Node representing the <item> tag of the RSS feed + + Returns: A tuple containing two strings representing title and URL respectively + """ + title = helpers.get_xml_text(item.getElementsByTagName('title')[0]) + try: + url = helpers.get_xml_text(item.getElementsByTagName('link')[0]) + if url: + url = url.replace('&','&') except IndexError: url = None - - return (title, url) - - def findEpisode (self, episode, manualSearch=False): - - self._checkAuth() - - logger.log(u"Searching "+self.name+" for " + episode.prettyName()) - - self.cache.updateCache() - results = self.cache.searchCache(episode, manualSearch) - logger.log(u"Cache results: "+str(results), logger.DEBUG) - - # if we got some results then use them no matter what. - # OR - # return anyway unless we're doing a manual search - if results or not manualSearch: - return results - - itemList = [] - - for cur_search_string in self._get_episode_search_strings(episode): - itemList += self._doSearch(cur_search_string, show=episode.show) - - for item in itemList: - - (title, url) = self._get_title_and_url(item) - - # parse the file name - try: - myParser = NameParser() - parse_result = myParser.parse(title) - except InvalidNameException: - logger.log(u"Unable to parse the filename "+title+" into a valid episode", logger.WARNING) - continue - - if episode.show.air_by_date: - if parse_result.air_date != episode.airdate: - logger.log("Episode "+title+" didn't air on "+str(episode.airdate)+", skipping it", logger.DEBUG) - continue - elif parse_result.season_number != episode.season or episode.episode not in parse_result.episode_numbers: - logger.log("Episode "+title+" isn't "+str(episode.season)+"x"+str(episode.episode)+", skipping it", logger.DEBUG) - continue - - quality = self.getQuality(item) - - if not episode.show.wantEpisode(episode.season, episode.episode, quality, manualSearch): - logger.log(u"Ignoring result "+title+" because we don't want an episode that is "+Quality.qualityStrings[quality], logger.DEBUG) - continue - - logger.log(u"Found result " + title + " at " + url, logger.DEBUG) - - result = self.getResult([episode]) - if item.extraInfo: - result.extraInfo = item.extraInfo - result.url = url - result.name = title - result.quality = quality - - results.append(result) - - return results - - - - def findSeasonResults(self, show, season): - - itemList = [] - results = {} - - for curString in self._get_season_search_strings(show, season): - itemList += self._doSearch(curString, show=show, season=season) - - for item in itemList: - - (title, url) = self._get_title_and_url(item) - - quality = self.getQuality(item) - - # parse the file name - try: - myParser = NameParser(False) - parse_result = myParser.parse(title) - except InvalidNameException: - logger.log(u"Unable to parse the filename "+title+" into a valid episode", logger.WARNING) - continue - - if not show.air_by_date: - # this check is meaningless for non-season searches - if (parse_result.season_number != None and parse_result.season_number != season) or (parse_result.season_number == None and season != 1): - logger.log(u"The result "+title+" doesn't seem to be a valid episode for season "+str(season)+", ignoring") - continue - - # we just use the existing info for normal searches - actual_season = season - actual_episodes = parse_result.episode_numbers - - else: - if not parse_result.air_by_date: - logger.log(u"This is supposed to be an air-by-date search but the result "+title+" didn't parse as one, skipping it", logger.DEBUG) - continue - - myDB = db.DBConnection() - sql_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE showid = ? AND airdate = ?", [show.tvdbid, parse_result.air_date.toordinal()]) - - if len(sql_results) != 1: - logger.log(u"Tried to look up the date for the episode "+title+" but the database didn't give proper results, skipping it", logger.WARNING) - continue - - actual_season = int(sql_results[0]["season"]) - actual_episodes = [int(sql_results[0]["episode"])] - - # make sure we want the episode - wantEp = True - for epNo in actual_episodes: - if not show.wantEpisode(actual_season, epNo, quality): - wantEp = False - break - - if not wantEp: - logger.log(u"Ignoring result "+title+" because we don't want an episode that is "+Quality.qualityStrings[quality], logger.DEBUG) - continue - - logger.log(u"Found result " + title + " at " + url, logger.DEBUG) - - # make a result object - epObj = [] - for curEp in actual_episodes: - epObj.append(show.getEpisode(actual_season, curEp)) - - result = self.getResult(epObj) - if item.extraInfo: - result.extraInfo = item.extraInfo - result.url = url - result.name = title - result.quality = quality - - if len(epObj) == 1: - epNum = epObj[0].episode - elif len(epObj) > 1: - epNum = MULTI_EP_RESULT - logger.log(u"Separating multi-episode result to check for later - result contains episodes: "+str(parse_result.episode_numbers), logger.DEBUG) - elif len(epObj) == 0: - epNum = SEASON_RESULT - if result.extraInfo: - result.extraInfo.append( show ) - else: - result.extraInfo = [show] - logger.log(u"Separating full season result to check for later", logger.DEBUG) - - if epNum in results: - results[epNum].append(result) - else: - results[epNum] = [result] - - - return results - - def findPropers(self, date=None): - - results = self.cache.listPropers(date) - - return [classes.Proper(x['name'], x['url'], datetime.datetime.fromtimestamp(x['time'])) for x in results] - - -class NZBProvider(GenericProvider): - - def __init__(self, name): - - GenericProvider.__init__(self, name) - - self.providerType = GenericProvider.NZB - -class TorrentProvider(GenericProvider): - - def __init__(self, name): - - GenericProvider.__init__(self, name) - - self.providerType = GenericProvider.TORRENT + + return (title, url) + + def _get_language(self,title=None,item=None): + return 'en' + + def findEpisode (self, episode, manualSearch=False): + + self._checkAuth() + + logger.log(u"Searching "+self.name+" for " + episode.prettyName()) + + self.cache.updateCache() + results = self.cache.searchCache(episode, manualSearch) + logger.log(u"Cache results: "+str(results), logger.DEBUG) + + # if we got some results then use them no matter what. + # OR + # return anyway unless we're doing a manual search + if results or not manualSearch: + return results + + itemList = [] + + for cur_search_string in self._get_episode_search_strings(episode): + itemList += self._doSearch(cur_search_string, show=episode.show) + + for item in itemList: + + (title, url) = self._get_title_and_url(item) + + # parse the file name + try: + myParser = NameParser() + parse_result = myParser.parse(title) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+title+" into a valid episode", logger.WARNING) + continue + + language = self._get_language(title,item) + + if episode.show.air_by_date: + if parse_result.air_date != episode.airdate: + logger.log("Episode "+title+" didn't air on "+str(episode.airdate)+", skipping it", logger.DEBUG) + continue + elif parse_result.season_number != episode.season or episode.episode not in parse_result.episode_numbers: + logger.log("Episode "+title+" isn't "+str(episode.season)+"x"+str(episode.episode)+", skipping it", logger.DEBUG) + continue + + quality = self.getQuality(item) + + if not episode.show.wantEpisode(episode.season, episode.episode, quality, manualSearch): + logger.log(u"Ignoring result "+title+" because we don't want an episode that is "+Quality.qualityStrings[quality], logger.DEBUG) + continue + + if not language == episode.show.audio_lang: + logger.log(u"Ignoring result "+title+" because the language: " + showLanguages[language] + " does not match the desired language: " + showLanguages[episode.show.show_lang]) + continue + + logger.log(u"Found result " + title + " at " + url, logger.DEBUG) + + result = self.getResult([episode]) + if item.extraInfo: + result.extraInfo = item.extraInfo + result.url = url + result.name = title + result.quality = quality + result.audio_langs = [language] + + results.append(result) + + return results + + + + def findSeasonResults(self, show, season): + + itemList = [] + results = {} + + for curString in self._get_season_search_strings(show, season): + itemList += self._doSearch(curString, show=show, season=season) + + for item in itemList: + + (title, url) = self._get_title_and_url(item) + + quality = self.getQuality(item) + + # parse the file name + try: + myParser = NameParser(False) + parse_result = myParser.parse(title) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+title+" into a valid episode", logger.WARNING) + continue + + language = self._get_language(title,item) + + if not show.air_by_date: + # this check is meaningless for non-season searches + if (parse_result.season_number != None and parse_result.season_number != season) or (parse_result.season_number == None and season != 1): + logger.log(u"The result "+title+" doesn't seem to be a valid episode for season "+str(season)+", ignoring") + continue + + # we just use the existing info for normal searches + actual_season = season + actual_episodes = parse_result.episode_numbers + + else: + if not parse_result.air_by_date: + logger.log(u"This is supposed to be an air-by-date search but the result "+title+" didn't parse as one, skipping it", logger.DEBUG) + continue + + myDB = db.DBConnection() + sql_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE showid = ? AND airdate = ?", [show.tvdbid, parse_result.air_date.toordinal()]) + + if len(sql_results) != 1: + logger.log(u"Tried to look up the date for the episode "+title+" but the database didn't give proper results, skipping it", logger.WARNING) + continue + + actual_season = int(sql_results[0]["season"]) + actual_episodes = [int(sql_results[0]["episode"])] + + # make sure we want the episode + wantEp = True + for epNo in actual_episodes: + if not show.wantEpisode(actual_season, epNo, quality): + wantEp = False + break + + if not wantEp: + logger.log(u"Ignoring result "+title+" because we don't want an episode that is "+Quality.qualityStrings[quality], logger.DEBUG) + continue + + if not language == show.show_lang: + logger.log(u"Ignoring result "+title+" because the language: " + showLanguages[parse_result.series_language] + " does not match the desired language: " + showLanguages[show.show_lang]) + continue + + logger.log(u"Found result " + title + " at " + url, logger.DEBUG) + + # make a result object + epObj = [] + for curEp in actual_episodes: + epObj.append(show.getEpisode(actual_season, curEp)) + + result = self.getResult(epObj) + if item.extraInfo: + result.extraInfo = item.extraInfo + result.url = url + result.name = title + result.quality = quality + result.audio_langs = [language] + + if len(epObj) == 1: + epNum = epObj[0].episode + elif len(epObj) > 1: + epNum = MULTI_EP_RESULT + logger.log(u"Separating multi-episode result to check for later - result contains episodes: "+str(parse_result.episode_numbers), logger.DEBUG) + elif len(epObj) == 0: + epNum = SEASON_RESULT + if result.extraInfo: + result.extraInfo.append( show ) + else: + result.extraInfo = [show] + logger.log(u"Separating full season result to check for later", logger.DEBUG) + + if epNum in results: + results[epNum].append(result) + else: + results[epNum] = [result] + + + return results + + def findPropers(self, date=None): + + results = self.cache.listPropers(date) + + return [classes.Proper(x['name'], x['url'], datetime.datetime.fromtimestamp(x['time'])) for x in results] + + +class NZBProvider(GenericProvider): + + def __init__(self, name): + + GenericProvider.__init__(self, name) + + self.providerType = GenericProvider.NZB + +class TorrentProvider(GenericProvider): + + def __init__(self, name): + + GenericProvider.__init__(self, name) + + self.providerType = GenericProvider.TORRENT diff --git a/sickbeard/providers/newznab.py b/sickbeard/providers/newznab.py index b696889d7a29f0c2d371aa6a2722161007860fe0..ab1813121ef7efc9f49d506dcb183189fc622327 100644 --- a/sickbeard/providers/newznab.py +++ b/sickbeard/providers/newznab.py @@ -1,310 +1,334 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import urllib -import email.utils -import datetime -import re -import os - -from xml.dom.minidom import parseString - -import sickbeard -import generic - -from sickbeard import classes -from sickbeard import helpers -from sickbeard import scene_exceptions -from sickbeard import encodingKludge as ek - -from sickbeard import exceptions -from sickbeard import logger -from sickbeard import tvcache -from sickbeard.exceptions import ex - - -class NewznabProvider(generic.NZBProvider): - - def __init__(self, name, url, key=''): - - generic.NZBProvider.__init__(self, name) - - self.cache = NewznabCache(self) - - self.url = url - self.key = key - - # if a provider doesn't need an api key then this can be false - self.needs_auth = True - - self.enabled = True - self.supportsBacklog = True - - self.default = False - - def configStr(self): - return self.name + '|' + self.url + '|' + self.key + '|' + str(int(self.enabled)) - - def imageName(self): - if ek.ek(os.path.isfile, ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', 'providers', self.getID() + '.png')): - return self.getID() + '.png' - return 'newznab.png' - - def isEnabled(self): - return self.enabled - - def _get_season_search_strings(self, show, season=None): - - if not show: - return [{}] - - to_return = [] - - # add new query strings for exceptions - name_exceptions = scene_exceptions.get_scene_exceptions(show.tvdbid) + [show.name] - for cur_exception in name_exceptions: - - cur_params = {} - - # search directly by tvrage id - if show.tvrid: - cur_params['rid'] = show.tvrid - # if we can't then fall back on a very basic name search - else: - cur_params['q'] = helpers.sanitizeSceneName(cur_exception) - - if season != None: - # air-by-date means &season=2010&q=2010.03, no other way to do it atm - if show.air_by_date: - cur_params['season'] = season.split('-')[0] - if 'q' in cur_params: - cur_params['q'] += '.' + season.replace('-', '.') - else: - cur_params['q'] = season.replace('-', '.') - else: - cur_params['season'] = season - - # hack to only add a single result if it's a rageid search - if not ('rid' in cur_params and to_return): - to_return.append(cur_params) - - return to_return - - def _get_episode_search_strings(self, ep_obj): - - params = {} - - if not ep_obj: - return [params] - - # search directly by tvrage id - if ep_obj.show.tvrid: - params['rid'] = ep_obj.show.tvrid - # if we can't then fall back on a very basic name search - else: - params['q'] = helpers.sanitizeSceneName(ep_obj.show.name) - - if ep_obj.show.air_by_date: - date_str = str(ep_obj.airdate) - - params['season'] = date_str.partition('-')[0] - params['ep'] = date_str.partition('-')[2].replace('-', '/') - else: - params['season'] = ep_obj.season - params['ep'] = ep_obj.episode - - to_return = [params] - - # only do exceptions if we are searching by name - if 'q' in params: - - # add new query strings for exceptions - name_exceptions = scene_exceptions.get_scene_exceptions(ep_obj.show.tvdbid) - for cur_exception in name_exceptions: - - # don't add duplicates - if cur_exception == ep_obj.show.name: - continue - - cur_return = params.copy() - cur_return['q'] = helpers.sanitizeSceneName(cur_exception) - to_return.append(cur_return) - - return to_return - - def _doGeneralSearch(self, search_string): - return self._doSearch({'q': search_string}) - - def _checkAuthFromData(self, data): - - try: - parsedXML = parseString(data) - except Exception: - return False - - if parsedXML.documentElement.tagName == 'error': - code = parsedXML.documentElement.getAttribute('code') - if code == '100': - raise exceptions.AuthException("Your API key for " + self.name + " is incorrect, check your config.") - elif code == '101': - raise exceptions.AuthException("Your account on " + self.name + " has been suspended, contact the administrator.") - elif code == '102': - raise exceptions.AuthException("Your account isn't allowed to use the API on " + self.name + ", contact the administrator") - else: - logger.log(u"Unknown error given from " + self.name + ": "+parsedXML.documentElement.getAttribute('description'), logger.ERROR) - return False - - return True - - def _doSearch(self, search_params, show=None, max_age=0): - - params = {"t": "tvsearch", - "maxage": sickbeard.USENET_RETENTION, - "limit": 100, - "cat": '5030,5040'} - - # if max_age is set, use it, don't allow it to be missing - if max_age or not params['maxage']: - params['maxage'] = max_age - - # hack this in for now - if self.getID() == 'nzbs_org': - params['cat'] += ',5070,5090' - - if search_params: - params.update(search_params) - - if self.key: - params['apikey'] = self.key - - searchURL = self.url + 'api?' + urllib.urlencode(params) - - logger.log(u"Search url: " + searchURL, logger.DEBUG) - - data = self.getURL(searchURL) - - if not data: - return [] - - # hack this in until it's fixed server side - if not data.startswith('<?xml'): - data = '<?xml version="1.0" encoding="ISO-8859-1" ?>' + data - - try: - parsedXML = parseString(data) - items = parsedXML.getElementsByTagName('item') - except Exception, e: - logger.log(u"Error trying to load " + self.name + " RSS feed: " + ex(e), logger.ERROR) - logger.log(u"RSS data: " + data, logger.DEBUG) - return [] - - if not self._checkAuthFromData(data): - return [] - - if parsedXML.documentElement.tagName != 'rss': - logger.log(u"Resulting XML from " + self.name + " isn't RSS, not parsing it", logger.ERROR) - return [] - - results = [] - - for curItem in items: - (title, url) = self._get_title_and_url(curItem) - - if not title or not url: - logger.log(u"The XML returned from the " + self.name + " RSS feed is incomplete, this result is unusable: " + data, logger.ERROR) - continue - - results.append(curItem) - - return results - - def findPropers(self, date=None): - - search_terms = ['.proper.', '.repack.'] - results = [] - - cache_results = self.cache.listPropers(date) - results = [classes.Proper(x['name'], x['url'], datetime.datetime.fromtimestamp(x['time'])) for x in cache_results] - - for term in search_terms: - for curResult in self._doSearch({'q': term}, max_age=4): - - (title, url) = self._get_title_and_url(curResult) - - description_node = curResult.getElementsByTagName('pubDate')[0] - descriptionStr = helpers.get_xml_text(description_node) - - try: - # we could probably do dateStr = descriptionStr but we want date in this format - dateStr = re.search('(\w{3}, \d{1,2} \w{3} \d{4} \d\d:\d\d:\d\d) [\+\-]\d{4}', descriptionStr).group(1) - except: - dateStr = None - - if not dateStr: - logger.log(u"Unable to figure out the date for entry " + title + ", skipping it") - continue - else: - - resultDate = email.utils.parsedate(dateStr) - if resultDate: - resultDate = datetime.datetime(*resultDate[0:6]) - - if date == None or resultDate > date: - search_result = classes.Proper(title, url, resultDate) - results.append(search_result) - - return results - - -class NewznabCache(tvcache.TVCache): - - def __init__(self, provider): - - tvcache.TVCache.__init__(self, provider) - - # only poll newznab providers every 15 minutes max - self.minTime = 15 - - def _getRSSData(self): - - params = {"t": "tvsearch", - "age": sickbeard.USENET_RETENTION, - "cat": '5040,5030'} - - # hack this in for now - if self.provider.getID() == 'nzbs_org': - params['cat'] += ',5070,5090' - - if self.provider.key: - params['apikey'] = self.provider.key - - url = self.provider.url + 'api?' + urllib.urlencode(params) - - logger.log(self.provider.name + " cache update URL: " + url, logger.DEBUG) - - data = self.provider.getURL(url) - - # hack this in until it's fixed server side - if data and not data.startswith('<?xml'): - data = '<?xml version="1.0" encoding="ISO-8859-1" ?>' + data - - return data - - def _checkAuth(self, data): - - return self.provider._checkAuthFromData(data) +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import urllib +import email.utils +import datetime +import re +import os + +from xml.dom.minidom import parseString + +import sickbeard +import generic + +from sickbeard import classes +from sickbeard import helpers +from sickbeard import scene_exceptions +from sickbeard import encodingKludge as ek + +from sickbeard import exceptions +from sickbeard import logger +from sickbeard import tvcache +from sickbeard.exceptions import ex +from sickbeard.name_parser.parser import NameParser, InvalidNameException + +class NewznabProvider(generic.NZBProvider): + + def __init__(self, name, url, key=''): + + generic.NZBProvider.__init__(self, name) + + self.cache = NewznabCache(self) + + self.url = url + self.key = key + + # if a provider doesn't need an api key then this can be false + self.needs_auth = True + + self.enabled = True + self.supportsBacklog = True + + self.default = False + + def configStr(self): + return self.name + '|' + self.url + '|' + self.key + '|' + str(int(self.enabled)) + + def imageName(self): + if ek.ek(os.path.isfile, ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', 'providers', self.getID() + '.png')): + return self.getID() + '.png' + return 'newznab.png' + + def isEnabled(self): + return self.enabled + + def _get_season_search_strings(self, show, season=None): + + if not show: + return [{}] + + to_return = [] + + # add new query strings for exceptions + name_exceptions = scene_exceptions.get_scene_exceptions(show.tvdbid) + [show.name] + for cur_exception in name_exceptions: + + cur_params = {} + + # search directly by tvrage id + if show.tvrid: + cur_params['rid'] = show.tvrid + # if we can't then fall back on a very basic name search + else: + cur_params['q'] = helpers.sanitizeSceneName(cur_exception) + + if season != None: + # air-by-date means &season=2010&q=2010.03, no other way to do it atm + if show.air_by_date: + cur_params['season'] = season.split('-')[0] + if 'q' in cur_params: + cur_params['q'] += '.' + season.replace('-', '.') + else: + cur_params['q'] = season.replace('-', '.') + else: + cur_params['season'] = season + + # hack to only add a single result if it's a rageid search + if not ('rid' in cur_params and to_return): + to_return.append(cur_params) + + return to_return + + def _get_episode_search_strings(self, ep_obj): + + params = {} + + if not ep_obj: + return [params] + + # search directly by tvrage id + if ep_obj.show.tvrid: + params['rid'] = ep_obj.show.tvrid + # if we can't then fall back on a very basic name search + else: + params['q'] = helpers.sanitizeSceneName(ep_obj.show.name) + + if ep_obj.show.air_by_date: + date_str = str(ep_obj.airdate) + + params['season'] = date_str.partition('-')[0] + params['ep'] = date_str.partition('-')[2].replace('-', '/') + else: + params['season'] = ep_obj.season + params['ep'] = ep_obj.episode + + to_return = [params] + + # only do exceptions if we are searching by name + if 'q' in params: + + # add new query strings for exceptions + name_exceptions = scene_exceptions.get_scene_exceptions(ep_obj.show.tvdbid) + for cur_exception in name_exceptions: + + # don't add duplicates + if cur_exception == ep_obj.show.name: + continue + + cur_return = params.copy() + cur_return['q'] = helpers.sanitizeSceneName(cur_exception) + to_return.append(cur_return) + + return to_return + + def _get_language(self, title=None, item=None): + if not title: + return 'en' + else: + try: + myParser = NameParser() + parse_result = myParser.parse(title) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+title+" into a valid episode", logger.WARNING) + return 'en' + + return parse_result.series_language + + def _doGeneralSearch(self, search_string): + return self._doSearch({'q': search_string}) + + def _checkAuthFromData(self, data): + + try: + parsedXML = parseString(data) + except Exception: + return False + + if parsedXML.documentElement.tagName == 'error': + code = parsedXML.documentElement.getAttribute('code') + if code == '100': + raise exceptions.AuthException("Your API key for " + self.name + " is incorrect, check your config.") + elif code == '101': + raise exceptions.AuthException("Your account on " + self.name + " has been suspended, contact the administrator.") + elif code == '102': + raise exceptions.AuthException("Your account isn't allowed to use the API on " + self.name + ", contact the administrator") + else: + logger.log(u"Unknown error given from " + self.name + ": "+parsedXML.documentElement.getAttribute('description'), logger.ERROR) + return False + + return True + + def _doSearch(self, search_params, show=None, max_age=0): + + cat = '5030,5040' + if show and show.audio_lang != u"en": + cat = '5020' + + params = {"t": "tvsearch", + "maxage": sickbeard.USENET_RETENTION, + "limit": 100, + "cat": cat} + + # if max_age is set, use it, don't allow it to be missing + if max_age or not params['maxage']: + params['maxage'] = max_age + + # hack this in for now + if self.getID() == 'nzbs_org': + params['cat'] += ',5070,5090' + + if search_params: + params.update(search_params) + + if self.key: + params['apikey'] = self.key + + searchURL = self.url + 'api?' + urllib.urlencode(params) + + logger.log(u"Search url: " + searchURL, logger.DEBUG) + + data = self.getURL(searchURL) + + if not data: + return [] + + # hack this in until it's fixed server side + if not data.startswith('<?xml'): + data = '<?xml version="1.0" encoding="ISO-8859-1" ?>' + data + + try: + parsedXML = parseString(data) + items = parsedXML.getElementsByTagName('item') + except Exception, e: + logger.log(u"Error trying to load " + self.name + " RSS feed: " + ex(e), logger.ERROR) + logger.log(u"RSS data: " + data, logger.DEBUG) + return [] + + if not self._checkAuthFromData(data): + return [] + + if parsedXML.documentElement.tagName != 'rss': + logger.log(u"Resulting XML from " + self.name + " isn't RSS, not parsing it", logger.ERROR) + return [] + + results = [] + + for curItem in items: + (title, url) = self._get_title_and_url(curItem) + + if not title or not url: + logger.log(u"The XML returned from the " + self.name + " RSS feed is incomplete, this result is unusable: " + data, logger.ERROR) + continue + + results.append(curItem) + + return results + + def findPropers(self, date=None): + + search_terms = ['.proper.', '.repack.'] + results = [] + + cache_results = self.cache.listPropers(date) + results = [classes.Proper(x['name'], x['url'], datetime.datetime.fromtimestamp(x['time'])) for x in cache_results] + + for term in search_terms: + for curResult in self._doSearch({'q': term}, max_age=4): + + (title, url) = self._get_title_and_url(curResult) + + description_node = curResult.getElementsByTagName('pubDate')[0] + descriptionStr = helpers.get_xml_text(description_node) + + try: + # we could probably do dateStr = descriptionStr but we want date in this format + dateStr = re.search('(\w{3}, \d{1,2} \w{3} \d{4} \d\d:\d\d:\d\d) [\+\-]\d{4}', descriptionStr).group(1) + except: + dateStr = None + + if not dateStr: + logger.log(u"Unable to figure out the date for entry " + title + ", skipping it") + continue + else: + + resultDate = email.utils.parsedate(dateStr) + if resultDate: + resultDate = datetime.datetime(*resultDate[0:6]) + + if date == None or resultDate > date: + search_result = classes.Proper(title, url, resultDate) + results.append(search_result) + + return results + + +class NewznabCache(tvcache.TVCache): + + def __init__(self, provider): + + tvcache.TVCache.__init__(self, provider) + + # only poll newznab providers every 15 minutes max + self.minTime = 15 + + def _getRSSData(self): + + languages = helpers.getAllLanguages() + + languages = filter(lambda x: not x == u"en", languages) + cat = '5030,5040' + if len(languages) > 0: + cat = '5020' + + params = {"t": "tvsearch", + "age": sickbeard.USENET_RETENTION, + "cat": cat} + + # hack this in for now + if self.provider.getID() == 'nzbs_org': + params['cat'] += ',5070,5090' + + if self.provider.key: + params['apikey'] = self.provider.key + + url = self.provider.url + 'api?' + urllib.urlencode(params) + + logger.log(self.provider.name + " cache update URL: " + url, logger.DEBUG) + + data = self.provider.getURL(url) + + # hack this in until it's fixed server side + if data and not data.startswith('<?xml'): + data = '<?xml version="1.0" encoding="ISO-8859-1" ?>' + data + + return data + + def _checkAuth(self, data): + + return self.provider._checkAuthFromData(data) diff --git a/sickbeard/search.py b/sickbeard/search.py index a51fece9774891a2b832739c903315063c15749d..f2f1951db29042fd724a342f420398a9152d7603 100644 --- a/sickbeard/search.py +++ b/sickbeard/search.py @@ -1,522 +1,523 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import os -import traceback - -import sickbeard - -from common import SNATCHED, Quality, SEASON_RESULT, MULTI_EP_RESULT - -from sickbeard import logger, db, show_name_helpers, exceptions, helpers -from sickbeard import sab -from sickbeard import nzbget -from sickbeard import history -from sickbeard import notifiers -from sickbeard import nzbSplitter -from sickbeard import ui -from sickbeard import encodingKludge as ek -from sickbeard import providers - -from sickbeard.exceptions import ex -from sickbeard.providers.generic import GenericProvider - -def _downloadResult(result): - """ - Downloads a result to the appropriate black hole folder. - - Returns a bool representing success. - - result: SearchResult instance to download. - """ - - resProvider = result.provider - - newResult = False - - if resProvider == None: - logger.log(u"Invalid provider name - this is a coding error, report it please", logger.ERROR) - return False - - # nzbs with an URL can just be downloaded from the provider - if result.resultType == "nzb": - newResult = resProvider.downloadResult(result) - - # if it's an nzb data result - elif result.resultType == "nzbdata": - - # get the final file path to the nzb - fileName = ek.ek(os.path.join, sickbeard.NZB_DIR, result.name + ".nzb") - - logger.log(u"Saving NZB to " + fileName) - - newResult = True - - # save the data to disk - try: - fileOut = open(fileName, "w") - fileOut.write(result.extraInfo[0]) - fileOut.close() - helpers.chmodAsParent(fileName) - except IOError, e: - logger.log(u"Error trying to save NZB to black hole: "+ex(e), logger.ERROR) - newResult = False - - elif result.resultType == "torrentdata": - - # get the final file path to the nzb - fileName = ek.ek(os.path.join, sickbeard.TORRENT_DIR, result.name + ".torrent") - - logger.log(u"Saving Torrent to " + fileName) - - newResult = True - - # save the data to disk - try: - fileOut = open(fileName, "wb") - fileOut.write(result.extraInfo[0]) - fileOut.close() - helpers.chmodAsParent(fileName) - except IOError, e: - logger.log(u"Error trying to save Torrent to black hole: "+ex(e), logger.ERROR) - newResult = False - - elif resProvider.providerType == "torrent": - newResult = resProvider.downloadResult(result) - - else: - logger.log(u"Invalid provider type - this is a coding error, report it please", logger.ERROR) - return False - - if newResult: - ui.notifications.message('Episode snatched','<b>%s</b> snatched from <b>%s</b>' % (result.name, resProvider.name)) - - return newResult - -def snatchEpisode(result, endStatus=SNATCHED): - """ - Contains the internal logic necessary to actually "snatch" a result that - has been found. - - Returns a bool representing success. - - result: SearchResult instance to be snatched. - endStatus: the episode status that should be used for the episode object once it's snatched. - """ - - # NZBs can be sent straight to SAB or saved to disk - if result.resultType in ("nzb", "nzbdata"): - if sickbeard.NZB_METHOD == "blackhole": - dlResult = _downloadResult(result) - elif sickbeard.NZB_METHOD == "sabnzbd": - dlResult = sab.sendNZB(result) - elif sickbeard.NZB_METHOD == "nzbget": - dlResult = nzbget.sendNZB(result) - else: - logger.log(u"Unknown NZB action specified in config: " + sickbeard.NZB_METHOD, logger.ERROR) - dlResult = False - - # torrents are always saved to disk - elif result.resultType in ("torrent", "torrentdata"): - dlResult = _downloadResult(result) - else: - logger.log(u"Unknown result type, unable to download it", logger.ERROR) - dlResult = False - - if dlResult == False: - return False - - history.logSnatch(result) - - # don't notify when we re-download an episode - for curEpObj in result.episodes: - with curEpObj.lock: - curEpObj.status = Quality.compositeStatus(endStatus, result.quality) - curEpObj.saveToDB() - - if curEpObj.status not in Quality.DOWNLOADED: - notifiers.notify_snatch(curEpObj.prettyName()) - - return True - -def searchForNeededEpisodes(): - - logger.log(u"Searching all providers for any needed episodes") - - foundResults = {} - - didSearch = False - - # ask all providers for any episodes it finds - for curProvider in providers.sortedProviderList(): - - if not curProvider.isActive(): - continue - - curFoundResults = {} - - try: - curFoundResults = curProvider.searchRSS() - except exceptions.AuthException, e: - logger.log(u"Authentication error: "+ex(e), logger.ERROR) - continue - except Exception, e: - logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - continue - - didSearch = True - - # pick a single result for each episode, respecting existing results - for curEp in curFoundResults: - - if curEp.show.paused: - logger.log(u"Show "+curEp.show.name+" is paused, ignoring all RSS items for "+curEp.prettyName(), logger.DEBUG) - continue - - # find the best result for the current episode - bestResult = None - for curResult in curFoundResults[curEp]: - if not bestResult or bestResult.quality < curResult.quality: - bestResult = curResult - - bestResult = pickBestResult(curFoundResults[curEp]) - - # if it's already in the list (from another provider) and the newly found quality is no better then skip it - if curEp in foundResults and bestResult.quality <= foundResults[curEp].quality: - continue - - foundResults[curEp] = bestResult - - if not didSearch: - logger.log(u"No NZB/Torrent providers found or enabled in the sickbeard config. Please check your settings.", logger.ERROR) - - return foundResults.values() - - -def pickBestResult(results, quality_list=None): - - logger.log(u"Picking the best result out of "+str([x.name for x in results]), logger.DEBUG) - - # find the best result for the current episode - bestResult = None - for cur_result in results: - logger.log("Quality of "+cur_result.name+" is "+Quality.qualityStrings[cur_result.quality]) - - if quality_list and cur_result.quality not in quality_list: - logger.log(cur_result.name+" is a quality we know we don't want, rejecting it", logger.DEBUG) - continue - - if not bestResult or bestResult.quality < cur_result.quality and cur_result.quality != Quality.UNKNOWN: - bestResult = cur_result - elif bestResult.quality == cur_result.quality: - if "proper" in cur_result.name.lower() or "repack" in cur_result.name.lower(): - bestResult = cur_result - elif "internal" in bestResult.name.lower() and "internal" not in cur_result.name.lower(): - bestResult = cur_result - - if bestResult: - logger.log(u"Picked "+bestResult.name+" as the best", logger.DEBUG) - else: - logger.log(u"No result picked.", logger.DEBUG) - - return bestResult - -def isFinalResult(result): - """ - Checks if the given result is good enough quality that we can stop searching for other ones. - - If the result is the highest quality in both the any/best quality lists then this function - returns True, if not then it's False - - """ - - logger.log(u"Checking if we should keep searching after we've found "+result.name, logger.DEBUG) - - show_obj = result.episodes[0].show - - any_qualities, best_qualities = Quality.splitQuality(show_obj.quality) - - # if there is a redownload that's higher than this then we definitely need to keep looking - if best_qualities and result.quality < max(best_qualities): - return False - - # if there's no redownload that's higher (above) and this is the highest initial download then we're good - elif any_qualities and result.quality == max(any_qualities): - return True - - elif best_qualities and result.quality == max(best_qualities): - - # if this is the best redownload but we have a higher initial download then keep looking - if any_qualities and result.quality < max(any_qualities): - return False - - # if this is the best redownload and we don't have a higher initial download then we're done - else: - return True - - # if we got here than it's either not on the lists, they're empty, or it's lower than the highest required - else: - return False - - -def findEpisode(episode, manualSearch=False): - - logger.log(u"Searching for " + episode.prettyName()) - - foundResults = [] - - didSearch = False - - for curProvider in providers.sortedProviderList(): - - if not curProvider.isActive(): - continue - - try: - curFoundResults = curProvider.findEpisode(episode, manualSearch=manualSearch) - except exceptions.AuthException, e: - logger.log(u"Authentication error: "+ex(e), logger.ERROR) - continue - except Exception, e: - logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - continue - - didSearch = True - - # skip non-tv crap - curFoundResults = filter(lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, episode.show), curFoundResults) - - # loop all results and see if any of them are good enough that we can stop searching - done_searching = False - for cur_result in curFoundResults: - done_searching = isFinalResult(cur_result) - logger.log(u"Should we stop searching after finding "+cur_result.name+": "+str(done_searching), logger.DEBUG) - if done_searching: - break - - foundResults += curFoundResults - - # if we did find a result that's good enough to stop then don't continue - if done_searching: - break - - if not didSearch: - logger.log(u"No NZB/Torrent providers found or enabled in the sickbeard config. Please check your settings.", logger.ERROR) - - bestResult = pickBestResult(foundResults) - - return bestResult - -def findSeason(show, season): - - logger.log(u"Searching for stuff we need from "+show.name+" season "+str(season)) - - foundResults = {} - - didSearch = False - - for curProvider in providers.sortedProviderList(): - - if not curProvider.isActive(): - continue - - try: - curResults = curProvider.findSeasonResults(show, season) - - # make a list of all the results for this provider - for curEp in curResults: - - # skip non-tv crap - curResults[curEp] = filter(lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, show), curResults[curEp]) - - if curEp in foundResults: - foundResults[curEp] += curResults[curEp] - else: - foundResults[curEp] = curResults[curEp] - - except exceptions.AuthException, e: - logger.log(u"Authentication error: "+ex(e), logger.ERROR) - continue - except Exception, e: - logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - continue - - didSearch = True - - if not didSearch: - logger.log(u"No NZB/Torrent providers found or enabled in the sickbeard config. Please check your settings.", logger.ERROR) - - finalResults = [] - - anyQualities, bestQualities = Quality.splitQuality(show.quality) - - # pick the best season NZB - bestSeasonNZB = None - if SEASON_RESULT in foundResults: - bestSeasonNZB = pickBestResult(foundResults[SEASON_RESULT], anyQualities+bestQualities) - - highest_quality_overall = 0 - for cur_season in foundResults: - for cur_result in foundResults[cur_season]: - if cur_result.quality != Quality.UNKNOWN and cur_result.quality > highest_quality_overall: - highest_quality_overall = cur_result.quality - logger.log(u"The highest quality of any match is "+Quality.qualityStrings[highest_quality_overall], logger.DEBUG) - - # see if every episode is wanted - if bestSeasonNZB: - - # get the quality of the season nzb - seasonQual = Quality.nameQuality(bestSeasonNZB.name) - seasonQual = bestSeasonNZB.quality - logger.log(u"The quality of the season NZB is "+Quality.qualityStrings[seasonQual], logger.DEBUG) - - myDB = db.DBConnection() - allEps = [int(x["episode"]) for x in myDB.select("SELECT episode FROM tv_episodes WHERE showid = ? AND season = ?", [show.tvdbid, season])] - logger.log(u"Episode list: "+str(allEps), logger.DEBUG) - - allWanted = True - anyWanted = False - for curEpNum in allEps: - if not show.wantEpisode(season, curEpNum, seasonQual): - allWanted = False - else: - anyWanted = True - - # if we need every ep in the season and there's nothing better then just download this and be done with it - if allWanted and bestSeasonNZB.quality == highest_quality_overall: - logger.log(u"Every ep in this season is needed, downloading the whole NZB "+bestSeasonNZB.name) - epObjs = [] - for curEpNum in allEps: - epObjs.append(show.getEpisode(season, curEpNum)) - bestSeasonNZB.episodes = epObjs - return [bestSeasonNZB] - - elif not anyWanted: - logger.log(u"No eps from this season are wanted at this quality, ignoring the result of "+bestSeasonNZB.name, logger.DEBUG) - - else: - - if bestSeasonNZB.provider.providerType == GenericProvider.NZB: - logger.log(u"Breaking apart the NZB and adding the individual ones to our results", logger.DEBUG) - - # if not, break it apart and add them as the lowest priority results - individualResults = nzbSplitter.splitResult(bestSeasonNZB) - - individualResults = filter(lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, show), individualResults) - - for curResult in individualResults: - if len(curResult.episodes) == 1: - epNum = curResult.episodes[0].episode - elif len(curResult.episodes) > 1: - epNum = MULTI_EP_RESULT - - if epNum in foundResults: - foundResults[epNum].append(curResult) - else: - foundResults[epNum] = [curResult] - - # If this is a torrent all we can do is leech the entire torrent, user will have to select which eps not do download in his torrent client - else: - - # Season result from BTN must be a full-season torrent, creating multi-ep result for it. - logger.log(u"Adding multi-ep result for full-season torrent. Set the episodes you don't want to 'don't download' in your torrent client if desired!") - epObjs = [] - for curEpNum in allEps: - epObjs.append(show.getEpisode(season, curEpNum)) - bestSeasonNZB.episodes = epObjs - - epNum = MULTI_EP_RESULT - if epNum in foundResults: - foundResults[epNum].append(bestSeasonNZB) - else: - foundResults[epNum] = [bestSeasonNZB] - - # go through multi-ep results and see if we really want them or not, get rid of the rest - multiResults = {} - if MULTI_EP_RESULT in foundResults: - for multiResult in foundResults[MULTI_EP_RESULT]: - - logger.log(u"Seeing if we want to bother with multi-episode result "+multiResult.name, logger.DEBUG) - - # see how many of the eps that this result covers aren't covered by single results - neededEps = [] - notNeededEps = [] - for epObj in multiResult.episodes: - epNum = epObj.episode - # if we have results for the episode - if epNum in foundResults and len(foundResults[epNum]) > 0: - # but the multi-ep is worse quality, we don't want it - # TODO: wtf is this False for - #if False and multiResult.quality <= pickBestResult(foundResults[epNum]): - # notNeededEps.append(epNum) - #else: - neededEps.append(epNum) - else: - neededEps.append(epNum) - - logger.log(u"Single-ep check result is neededEps: "+str(neededEps)+", notNeededEps: "+str(notNeededEps), logger.DEBUG) - - if not neededEps: - logger.log(u"All of these episodes were covered by single nzbs, ignoring this multi-ep result", logger.DEBUG) - continue - - # check if these eps are already covered by another multi-result - multiNeededEps = [] - multiNotNeededEps = [] - for epObj in multiResult.episodes: - epNum = epObj.episode - if epNum in multiResults: - multiNotNeededEps.append(epNum) - else: - multiNeededEps.append(epNum) - - logger.log(u"Multi-ep check result is multiNeededEps: "+str(multiNeededEps)+", multiNotNeededEps: "+str(multiNotNeededEps), logger.DEBUG) - - if not multiNeededEps: - logger.log(u"All of these episodes were covered by another multi-episode nzbs, ignoring this multi-ep result", logger.DEBUG) - continue - - # if we're keeping this multi-result then remember it - for epObj in multiResult.episodes: - multiResults[epObj.episode] = multiResult - - # don't bother with the single result if we're going to get it with a multi result - for epObj in multiResult.episodes: - epNum = epObj.episode - if epNum in foundResults: - logger.log(u"A needed multi-episode result overlaps with a single-episode result for ep #"+str(epNum)+", removing the single-episode results from the list", logger.DEBUG) - del foundResults[epNum] - - finalResults += set(multiResults.values()) - - # of all the single ep results narrow it down to the best one for each episode - for curEp in foundResults: - if curEp in (MULTI_EP_RESULT, SEASON_RESULT): - continue - - if len(foundResults[curEp]) == 0: - continue - - finalResults.append(pickBestResult(foundResults[curEp])) - - return finalResults +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import os +import traceback + +import sickbeard + +from common import SNATCHED, Quality, SEASON_RESULT, MULTI_EP_RESULT + +from sickbeard import logger, db, show_name_helpers, exceptions, helpers +from sickbeard import sab +from sickbeard import nzbget +from sickbeard import history +from sickbeard import notifiers +from sickbeard import nzbSplitter +from sickbeard import ui +from sickbeard import encodingKludge as ek +from sickbeard import providers + +from sickbeard.exceptions import ex +from sickbeard.providers.generic import GenericProvider + +def _downloadResult(result): + """ + Downloads a result to the appropriate black hole folder. + + Returns a bool representing success. + + result: SearchResult instance to download. + """ + + resProvider = result.provider + + newResult = False + + if resProvider == None: + logger.log(u"Invalid provider name - this is a coding error, report it please", logger.ERROR) + return False + + # nzbs with an URL can just be downloaded from the provider + if result.resultType == "nzb": + newResult = resProvider.downloadResult(result) + + # if it's an nzb data result + elif result.resultType == "nzbdata": + + # get the final file path to the nzb + fileName = ek.ek(os.path.join, sickbeard.NZB_DIR, result.name + ".nzb") + + logger.log(u"Saving NZB to " + fileName) + + newResult = True + + # save the data to disk + try: + fileOut = open(fileName, "w") + fileOut.write(result.extraInfo[0]) + fileOut.close() + helpers.chmodAsParent(fileName) + except IOError, e: + logger.log(u"Error trying to save NZB to black hole: "+ex(e), logger.ERROR) + newResult = False + + elif result.resultType == "torrentdata": + + # get the final file path to the nzb + fileName = ek.ek(os.path.join, sickbeard.TORRENT_DIR, result.name + ".torrent") + + logger.log(u"Saving Torrent to " + fileName) + + newResult = True + + # save the data to disk + try: + fileOut = open(fileName, "wb") + fileOut.write(result.extraInfo[0]) + fileOut.close() + helpers.chmodAsParent(fileName) + except IOError, e: + logger.log(u"Error trying to save Torrent to black hole: "+ex(e), logger.ERROR) + newResult = False + + elif resProvider.providerType == "torrent": + newResult = resProvider.downloadResult(result) + + else: + logger.log(u"Invalid provider type - this is a coding error, report it please", logger.ERROR) + return False + + if newResult: + ui.notifications.message('Episode snatched','<b>%s</b> snatched from <b>%s</b>' % (result.name, resProvider.name)) + + return newResult + +def snatchEpisode(result, endStatus=SNATCHED): + """ + Contains the internal logic necessary to actually "snatch" a result that + has been found. + + Returns a bool representing success. + + result: SearchResult instance to be snatched. + endStatus: the episode status that should be used for the episode object once it's snatched. + """ + + # NZBs can be sent straight to SAB or saved to disk + if result.resultType in ("nzb", "nzbdata"): + if sickbeard.NZB_METHOD == "blackhole": + dlResult = _downloadResult(result) + elif sickbeard.NZB_METHOD == "sabnzbd": + dlResult = sab.sendNZB(result) + elif sickbeard.NZB_METHOD == "nzbget": + dlResult = nzbget.sendNZB(result) + else: + logger.log(u"Unknown NZB action specified in config: " + sickbeard.NZB_METHOD, logger.ERROR) + dlResult = False + + # torrents are always saved to disk + elif result.resultType in ("torrent", "torrentdata"): + dlResult = _downloadResult(result) + else: + logger.log(u"Unknown result type, unable to download it", logger.ERROR) + dlResult = False + + if dlResult == False: + return False + + history.logSnatch(result) + + # don't notify when we re-download an episode + for curEpObj in result.episodes: + with curEpObj.lock: + curEpObj.status = Quality.compositeStatus(endStatus, result.quality) + curEpObj.audio_langs = result.audio_langs + curEpObj.saveToDB() + + if curEpObj.status not in Quality.DOWNLOADED: + notifiers.notify_snatch(curEpObj.prettyName()) + + return True + +def searchForNeededEpisodes(): + + logger.log(u"Searching all providers for any needed episodes") + + foundResults = {} + + didSearch = False + + # ask all providers for any episodes it finds + for curProvider in providers.sortedProviderList(): + + if not curProvider.isActive(): + continue + + curFoundResults = {} + + try: + curFoundResults = curProvider.searchRSS() + except exceptions.AuthException, e: + logger.log(u"Authentication error: "+ex(e), logger.ERROR) + continue + except Exception, e: + logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + continue + + didSearch = True + + # pick a single result for each episode, respecting existing results + for curEp in curFoundResults: + + if curEp.show.paused: + logger.log(u"Show "+curEp.show.name+" is paused, ignoring all RSS items for "+curEp.prettyName(), logger.DEBUG) + continue + + # find the best result for the current episode + bestResult = None + for curResult in curFoundResults[curEp]: + if not bestResult or bestResult.quality < curResult.quality: + bestResult = curResult + + bestResult = pickBestResult(curFoundResults[curEp]) + + # if it's already in the list (from another provider) and the newly found quality is no better then skip it + if curEp in foundResults and bestResult.quality <= foundResults[curEp].quality: + continue + + foundResults[curEp] = bestResult + + if not didSearch: + logger.log(u"No NZB/Torrent providers found or enabled in the sickbeard config. Please check your settings.", logger.ERROR) + + return foundResults.values() + + +def pickBestResult(results, quality_list=None): + + logger.log(u"Picking the best result out of "+str([x.name for x in results]), logger.DEBUG) + + # find the best result for the current episode + bestResult = None + for cur_result in results: + logger.log("Quality of "+cur_result.name+" is "+Quality.qualityStrings[cur_result.quality]) + + if quality_list and cur_result.quality not in quality_list: + logger.log(cur_result.name+" is a quality we know we don't want, rejecting it", logger.DEBUG) + continue + + if not bestResult or bestResult.quality < cur_result.quality and cur_result.quality != Quality.UNKNOWN: + bestResult = cur_result + elif bestResult.quality == cur_result.quality: + if "proper" in cur_result.name.lower() or "repack" in cur_result.name.lower(): + bestResult = cur_result + elif "internal" in bestResult.name.lower() and "internal" not in cur_result.name.lower(): + bestResult = cur_result + + if bestResult: + logger.log(u"Picked "+bestResult.name+" as the best", logger.DEBUG) + else: + logger.log(u"No result picked.", logger.DEBUG) + + return bestResult + +def isFinalResult(result): + """ + Checks if the given result is good enough quality that we can stop searching for other ones. + + If the result is the highest quality in both the any/best quality lists then this function + returns True, if not then it's False + + """ + + logger.log(u"Checking if we should keep searching after we've found "+result.name, logger.DEBUG) + + show_obj = result.episodes[0].show + + any_qualities, best_qualities = Quality.splitQuality(show_obj.quality) + + # if there is a redownload that's higher than this then we definitely need to keep looking + if best_qualities and result.quality < max(best_qualities): + return False + + # if there's no redownload that's higher (above) and this is the highest initial download then we're good + elif any_qualities and result.quality == max(any_qualities): + return True + + elif best_qualities and result.quality == max(best_qualities): + + # if this is the best redownload but we have a higher initial download then keep looking + if any_qualities and result.quality < max(any_qualities): + return False + + # if this is the best redownload and we don't have a higher initial download then we're done + else: + return True + + # if we got here than it's either not on the lists, they're empty, or it's lower than the highest required + else: + return False + + +def findEpisode(episode, manualSearch=False): + + logger.log(u"Searching for " + episode.prettyName()) + + foundResults = [] + + didSearch = False + + for curProvider in providers.sortedProviderList(): + + if not curProvider.isActive(): + continue + + try: + curFoundResults = curProvider.findEpisode(episode, manualSearch=manualSearch) + except exceptions.AuthException, e: + logger.log(u"Authentication error: "+ex(e), logger.ERROR) + continue + except Exception, e: + logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + continue + + didSearch = True + + # skip non-tv crap + curFoundResults = filter(lambda x: show_name_helpers.filterBadReleases(x.name,episode.show.audio_lang) and show_name_helpers.isGoodResult(x.name, episode.show), curFoundResults) + + # loop all results and see if any of them are good enough that we can stop searching + done_searching = False + for cur_result in curFoundResults: + done_searching = isFinalResult(cur_result) + logger.log(u"Should we stop searching after finding "+cur_result.name+": "+str(done_searching), logger.DEBUG) + if done_searching: + break + + foundResults += curFoundResults + + # if we did find a result that's good enough to stop then don't continue + if done_searching: + break + + if not didSearch: + logger.log(u"No NZB/Torrent providers found or enabled in the sickbeard config. Please check your settings.", logger.ERROR) + + bestResult = pickBestResult(foundResults) + + return bestResult + +def findSeason(show, season): + + logger.log(u"Searching for stuff we need from "+show.name+" season "+str(season)) + + foundResults = {} + + didSearch = False + + for curProvider in providers.sortedProviderList(): + + if not curProvider.isActive(): + continue + + try: + curResults = curProvider.findSeasonResults(show, season) + + # make a list of all the results for this provider + for curEp in curResults: + + # skip non-tv crap + curResults[curEp] = filter(lambda x: show_name_helpers.filterBadReleases(x.name,show.audio_lang) and show_name_helpers.isGoodResult(x.name, show), curResults[curEp]) + + if curEp in foundResults: + foundResults[curEp] += curResults[curEp] + else: + foundResults[curEp] = curResults[curEp] + + except exceptions.AuthException, e: + logger.log(u"Authentication error: "+ex(e), logger.ERROR) + continue + except Exception, e: + logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + continue + + didSearch = True + + if not didSearch: + logger.log(u"No NZB/Torrent providers found or enabled in the sickbeard config. Please check your settings.", logger.ERROR) + + finalResults = [] + + anyQualities, bestQualities = Quality.splitQuality(show.quality) + + # pick the best season NZB + bestSeasonNZB = None + if SEASON_RESULT in foundResults: + bestSeasonNZB = pickBestResult(foundResults[SEASON_RESULT], anyQualities+bestQualities) + + highest_quality_overall = 0 + for cur_season in foundResults: + for cur_result in foundResults[cur_season]: + if cur_result.quality != Quality.UNKNOWN and cur_result.quality > highest_quality_overall: + highest_quality_overall = cur_result.quality + logger.log(u"The highest quality of any match is "+Quality.qualityStrings[highest_quality_overall], logger.DEBUG) + + # see if every episode is wanted + if bestSeasonNZB: + + # get the quality of the season nzb + seasonQual = Quality.nameQuality(bestSeasonNZB.name) + seasonQual = bestSeasonNZB.quality + logger.log(u"The quality of the season NZB is "+Quality.qualityStrings[seasonQual], logger.DEBUG) + + myDB = db.DBConnection() + allEps = [int(x["episode"]) for x in myDB.select("SELECT episode FROM tv_episodes WHERE showid = ? AND season = ?", [show.tvdbid, season])] + logger.log(u"Episode list: "+str(allEps), logger.DEBUG) + + allWanted = True + anyWanted = False + for curEpNum in allEps: + if not show.wantEpisode(season, curEpNum, seasonQual): + allWanted = False + else: + anyWanted = True + + # if we need every ep in the season and there's nothing better then just download this and be done with it + if allWanted and bestSeasonNZB.quality == highest_quality_overall: + logger.log(u"Every ep in this season is needed, downloading the whole NZB "+bestSeasonNZB.name) + epObjs = [] + for curEpNum in allEps: + epObjs.append(show.getEpisode(season, curEpNum)) + bestSeasonNZB.episodes = epObjs + return [bestSeasonNZB] + + elif not anyWanted: + logger.log(u"No eps from this season are wanted at this quality, ignoring the result of "+bestSeasonNZB.name, logger.DEBUG) + + else: + + if bestSeasonNZB.provider.providerType == GenericProvider.NZB: + logger.log(u"Breaking apart the NZB and adding the individual ones to our results", logger.DEBUG) + + # if not, break it apart and add them as the lowest priority results + individualResults = nzbSplitter.splitResult(bestSeasonNZB) + + individualResults = filter(lambda x: show_name_helpers.filterBadReleases(x.name,show.audio_lang) and show_name_helpers.isGoodResult(x.name, show), individualResults) + + for curResult in individualResults: + if len(curResult.episodes) == 1: + epNum = curResult.episodes[0].episode + elif len(curResult.episodes) > 1: + epNum = MULTI_EP_RESULT + + if epNum in foundResults: + foundResults[epNum].append(curResult) + else: + foundResults[epNum] = [curResult] + + # If this is a torrent all we can do is leech the entire torrent, user will have to select which eps not do download in his torrent client + else: + + # Season result from BTN must be a full-season torrent, creating multi-ep result for it. + logger.log(u"Adding multi-ep result for full-season torrent. Set the episodes you don't want to 'don't download' in your torrent client if desired!") + epObjs = [] + for curEpNum in allEps: + epObjs.append(show.getEpisode(season, curEpNum)) + bestSeasonNZB.episodes = epObjs + + epNum = MULTI_EP_RESULT + if epNum in foundResults: + foundResults[epNum].append(bestSeasonNZB) + else: + foundResults[epNum] = [bestSeasonNZB] + + # go through multi-ep results and see if we really want them or not, get rid of the rest + multiResults = {} + if MULTI_EP_RESULT in foundResults: + for multiResult in foundResults[MULTI_EP_RESULT]: + + logger.log(u"Seeing if we want to bother with multi-episode result "+multiResult.name, logger.DEBUG) + + # see how many of the eps that this result covers aren't covered by single results + neededEps = [] + notNeededEps = [] + for epObj in multiResult.episodes: + epNum = epObj.episode + # if we have results for the episode + if epNum in foundResults and len(foundResults[epNum]) > 0: + # but the multi-ep is worse quality, we don't want it + # TODO: wtf is this False for + #if False and multiResult.quality <= pickBestResult(foundResults[epNum]): + # notNeededEps.append(epNum) + #else: + neededEps.append(epNum) + else: + neededEps.append(epNum) + + logger.log(u"Single-ep check result is neededEps: "+str(neededEps)+", notNeededEps: "+str(notNeededEps), logger.DEBUG) + + if not neededEps: + logger.log(u"All of these episodes were covered by single nzbs, ignoring this multi-ep result", logger.DEBUG) + continue + + # check if these eps are already covered by another multi-result + multiNeededEps = [] + multiNotNeededEps = [] + for epObj in multiResult.episodes: + epNum = epObj.episode + if epNum in multiResults: + multiNotNeededEps.append(epNum) + else: + multiNeededEps.append(epNum) + + logger.log(u"Multi-ep check result is multiNeededEps: "+str(multiNeededEps)+", multiNotNeededEps: "+str(multiNotNeededEps), logger.DEBUG) + + if not multiNeededEps: + logger.log(u"All of these episodes were covered by another multi-episode nzbs, ignoring this multi-ep result", logger.DEBUG) + continue + + # if we're keeping this multi-result then remember it + for epObj in multiResult.episodes: + multiResults[epObj.episode] = multiResult + + # don't bother with the single result if we're going to get it with a multi result + for epObj in multiResult.episodes: + epNum = epObj.episode + if epNum in foundResults: + logger.log(u"A needed multi-episode result overlaps with a single-episode result for ep #"+str(epNum)+", removing the single-episode results from the list", logger.DEBUG) + del foundResults[epNum] + + finalResults += set(multiResults.values()) + + # of all the single ep results narrow it down to the best one for each episode + for curEp in foundResults: + if curEp in (MULTI_EP_RESULT, SEASON_RESULT): + continue + + if len(foundResults[curEp]) == 0: + continue + + finalResults.append(pickBestResult(foundResults[curEp])) + + return finalResults diff --git a/sickbeard/show_name_helpers.py b/sickbeard/show_name_helpers.py index f2971338897a35c4ac0a6adfd75a3114093315bb..eca6a02294ea00fdd8944f0e85a9646ff1d4ee10 100644 --- a/sickbeard/show_name_helpers.py +++ b/sickbeard/show_name_helpers.py @@ -1,266 +1,273 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import sickbeard - -from sickbeard.common import countryList -from sickbeard.helpers import sanitizeSceneName -from sickbeard.scene_exceptions import get_scene_exceptions -from sickbeard import logger -from sickbeard import db - -import re -import datetime - -from name_parser.parser import NameParser, InvalidNameException - -resultFilters = ["sub(pack|s|bed)", "nlsub(bed|s)?", "swesub(bed)?", - "(dir|sample|nfo)fix", "sample", "(dvd)?extras", - "dub(bed)?"] - -def filterBadReleases(name): - """ - Filters out non-english and just all-around stupid releases by comparing them - to the resultFilters contents. - - name: the release name to check - - Returns: True if the release name is OK, False if it's bad. - """ - - try: - fp = NameParser() - parse_result = fp.parse(name) - except InvalidNameException: - logger.log(u"Unable to parse the filename "+name+" into a valid episode", logger.WARNING) - return False - - # use the extra info and the scene group to filter against - check_string = '' - if parse_result.extra_info: - check_string = parse_result.extra_info - if parse_result.release_group: - if check_string: - check_string = check_string + '-' + parse_result.release_group - else: - check_string = parse_result.release_group - - # if there's no info after the season info then assume it's fine - if not check_string: - return True - - # if any of the bad strings are in the name then say no - for x in resultFilters + sickbeard.IGNORE_WORDS.split(','): - if re.search('(^|[\W_])'+x+'($|[\W_])', check_string, re.I): - logger.log(u"Invalid scene release: "+name+" contains "+x+", ignoring it", logger.DEBUG) - return False - - return True - -def sceneToNormalShowNames(name): - """ - Takes a show name from a scene dirname and converts it to a more "human-readable" format. - - name: The show name to convert - - Returns: a list of all the possible "normal" names - """ - - if not name: - return [] - - name_list = [name] - - # use both and and & - new_name = re.sub('(?i)([\. ])and([\. ])', '\\1&\\2', name, re.I) - if new_name not in name_list: - name_list.append(new_name) - - results = [] - - for cur_name in name_list: - # add brackets around the year - results.append(re.sub('(\D)(\d{4})$', '\\1(\\2)', cur_name)) - - # add brackets around the country - country_match_str = '|'.join(countryList.values()) - results.append(re.sub('(?i)([. _-])('+country_match_str+')$', '\\1(\\2)', cur_name)) - - results += name_list - - return list(set(results)) - -def makeSceneShowSearchStrings(show): - - showNames = allPossibleShowNames(show) - - # scenify the names - return map(sanitizeSceneName, showNames) - - -def makeSceneSeasonSearchString (show, segment, extraSearchType=None): - - myDB = db.DBConnection() - - if show.air_by_date: - numseasons = 0 - - # the search string for air by date shows is just - seasonStrings = [segment] - - else: - numseasonsSQlResult = myDB.select("SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0", [show.tvdbid]) - numseasons = int(numseasonsSQlResult[0][0]) - - seasonStrings = ["S%02d" % segment] - # since nzbmatrix allows more than one search per request we search SxEE results too - if extraSearchType == "nzbmatrix": - seasonStrings.append("%ix" % segment) - - showNames = set(makeSceneShowSearchStrings(show)) - - toReturn = [] - term_list = [] - - # search each show name - for curShow in showNames: - # most providers all work the same way - if not extraSearchType: - # if there's only one season then we can just use the show name straight up - if numseasons == 1: - toReturn.append(curShow) - # for providers that don't allow multiple searches in one request we only search for Sxx style stuff - else: - for cur_season in seasonStrings: - toReturn.append(curShow + "." + cur_season) - - # nzbmatrix is special, we build a search string just for them - elif extraSearchType == "nzbmatrix": - if numseasons == 1: - toReturn.append('"'+curShow+'"') - elif numseasons == 0: - toReturn.append('"'+curShow+' '+str(segment).replace('-',' ')+'"') - else: - term_list = [x+'*' for x in seasonStrings] - if show.air_by_date: - term_list = ['"'+x+'"' for x in term_list] - - toReturn.append('"'+curShow+'"') - - if extraSearchType == "nzbmatrix": - toReturn = ['+('+','.join(toReturn)+')'] - if term_list: - toReturn.append('+('+','.join(term_list)+')') - return toReturn - - -def makeSceneSearchString (episode): - - myDB = db.DBConnection() - numseasonsSQlResult = myDB.select("SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0", [episode.show.tvdbid]) - numseasons = int(numseasonsSQlResult[0][0]) - - # see if we should use dates instead of episodes - if episode.show.air_by_date and episode.airdate != datetime.date.fromordinal(1): - epStrings = [str(episode.airdate)] - else: - epStrings = ["S%02iE%02i" % (int(episode.season), int(episode.episode)), - "%ix%02i" % (int(episode.season), int(episode.episode))] - - # for single-season shows just search for the show name - if numseasons == 1: - epStrings = [''] - - showNames = set(makeSceneShowSearchStrings(episode.show)) - - toReturn = [] - - for curShow in showNames: - for curEpString in epStrings: - toReturn.append(curShow + '.' + curEpString) - - return toReturn - -def isGoodResult(name, show, log=True): - """ - Use an automatically-created regex to make sure the result actually is the show it claims to be - """ - - all_show_names = allPossibleShowNames(show) - showNames = map(sanitizeSceneName, all_show_names) + all_show_names - - for curName in set(showNames): - escaped_name = re.sub('\\\\[\\s.-]', '\W+', re.escape(curName)) - if show.startyear: - escaped_name += "(?:\W+"+str(show.startyear)+")?" - curRegex = '^' + escaped_name + '\W+(?:(?:S\d[\dE._ -])|(?:\d\d?x)|(?:\d{4}\W\d\d\W\d\d)|(?:(?:part|pt)[\._ -]?(\d|[ivx]))|Season\W+\d+\W+|E\d+\W+)' - if log: - logger.log(u"Checking if show "+name+" matches " + curRegex, logger.DEBUG) - - match = re.search(curRegex, name, re.I) - - if match: - logger.log(u"Matched "+curRegex+" to "+name, logger.DEBUG) - return True - - if log: - logger.log(u"Provider gave result "+name+" but that doesn't seem like a valid result for "+show.name+" so I'm ignoring it") - return False - -def allPossibleShowNames(show): - """ - Figures out every possible variation of the name for a particular show. Includes TVDB name, TVRage name, - country codes on the end, eg. "Show Name (AU)", and any scene exception names. - - show: a TVShow object that we should get the names of - - Returns: a list of all the possible show names - """ - - showNames = [show.name] - showNames += [name for name in get_scene_exceptions(show.tvdbid)] - - # if we have a tvrage name then use it - if show.tvrname != "" and show.tvrname != None: - showNames.append(show.tvrname) - - if show.custom_search_names != '': - for custom_name in show.custom_search_names.split(','): - showNames.append(custom_name) - - newShowNames = [] - - country_list = countryList - country_list.update(dict(zip(countryList.values(), countryList.keys()))) - - # if we have "Show Name Australia" or "Show Name (Australia)" this will add "Show Name (AU)" for - # any countries defined in common.countryList - # (and vice versa) - for curName in set(showNames): - if not curName: - continue - for curCountry in country_list: - if curName.endswith(' '+curCountry): - newShowNames.append(curName.replace(' '+curCountry, ' ('+country_list[curCountry]+')')) - elif curName.endswith(' ('+curCountry+')'): - newShowNames.append(curName.replace(' ('+curCountry+')', ' ('+country_list[curCountry]+')')) - - showNames += newShowNames - - return showNames - +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import sickbeard + +from sickbeard.common import countryList, showLanguages +from sickbeard.helpers import sanitizeSceneName +from sickbeard.scene_exceptions import get_scene_exceptions +from sickbeard import logger +from sickbeard import db + +import re +import datetime +import string + +from name_parser.parser import NameParser, InvalidNameException + +resultFilters = ["sub(pack|s|bed)", "nlsub(bed|s)?", "swesub(bed)?", + "(dir|sample|nfo)fix", "sample", "(dvd)?extras"] + + +def filterBadReleases(name,showLang=u"en"): + """ + Filters out non-english and just all-around stupid releases by comparing them + to the resultFilters contents. + + name: the release name to check + + Returns: True if the release name is OK, False if it's bad. + """ + + additionalFilters = [] + if showLang == u"en": + additionalFilters.append("dub(bed)?") + + try: + fp = NameParser() + parse_result = fp.parse(name) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+name+" into a valid episode", logger.WARNING) + return False + + # use the extra info and the scene group to filter against + check_string = '' + if parse_result.extra_info: + check_string = parse_result.extra_info + if parse_result.release_group: + if check_string: + check_string = check_string + '-' + parse_result.release_group + else: + check_string = parse_result.release_group + + # if there's no info after the season info then assume it's fine + if not check_string: + return True + + # if any of the bad strings are in the name then say no + for x in resultFilters + sickbeard.IGNORE_WORDS.split(',') + additionalFilters: + if x == showLanguages.get(showLang): + continue + if re.search('(^|[\W_])'+x+'($|[\W_])', check_string, re.I): + logger.log(u"Invalid scene release: "+name+" contains "+x+", ignoring it", logger.DEBUG) + return False + + return True + +def sceneToNormalShowNames(name): + """ + Takes a show name from a scene dirname and converts it to a more "human-readable" format. + + name: The show name to convert + + Returns: a list of all the possible "normal" names + """ + + if not name: + return [] + + name_list = [name] + + # use both and and & + new_name = re.sub('(?i)([\. ])and([\. ])', '\\1&\\2', name, re.I) + if new_name not in name_list: + name_list.append(new_name) + + results = [] + + for cur_name in name_list: + # add brackets around the year + results.append(re.sub('(\D)(\d{4})$', '\\1(\\2)', cur_name)) + + # add brackets around the country + country_match_str = '|'.join(countryList.values()) + results.append(re.sub('(?i)([. _-])('+country_match_str+')$', '\\1(\\2)', cur_name)) + + results += name_list + + return list(set(results)) + +def makeSceneShowSearchStrings(show): + + showNames = allPossibleShowNames(show) + + # scenify the names + return map(sanitizeSceneName, showNames) + + +def makeSceneSeasonSearchString (show, segment, extraSearchType=None): + + myDB = db.DBConnection() + + if show.air_by_date: + numseasons = 0 + + # the search string for air by date shows is just + seasonStrings = [segment] + + else: + numseasonsSQlResult = myDB.select("SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0", [show.tvdbid]) + numseasons = int(numseasonsSQlResult[0][0]) + + seasonStrings = ["S%02d" % segment] + # since nzbmatrix allows more than one search per request we search SxEE results too + if extraSearchType == "nzbmatrix": + seasonStrings.append("%ix" % segment) + + showNames = set(makeSceneShowSearchStrings(show)) + + toReturn = [] + term_list = [] + + # search each show name + for curShow in showNames: + # most providers all work the same way + if not extraSearchType: + # if there's only one season then we can just use the show name straight up + if numseasons == 1: + toReturn.append(curShow) + # for providers that don't allow multiple searches in one request we only search for Sxx style stuff + else: + for cur_season in seasonStrings: + toReturn.append(curShow + "." + cur_season) + + # nzbmatrix is special, we build a search string just for them + elif extraSearchType == "nzbmatrix": + if numseasons == 1: + toReturn.append('"'+curShow+'"') + elif numseasons == 0: + toReturn.append('"'+curShow+' '+str(segment).replace('-',' ')+'"') + else: + term_list = [x+'*' for x in seasonStrings] + if show.air_by_date: + term_list = ['"'+x+'"' for x in term_list] + + toReturn.append('"'+curShow+'"') + + if extraSearchType == "nzbmatrix": + toReturn = ['+('+','.join(toReturn)+')'] + if term_list: + toReturn.append('+('+','.join(term_list)+')') + return toReturn + + +def makeSceneSearchString (episode): + + myDB = db.DBConnection() + numseasonsSQlResult = myDB.select("SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0", [episode.show.tvdbid]) + numseasons = int(numseasonsSQlResult[0][0]) + + # see if we should use dates instead of episodes + if episode.show.air_by_date and episode.airdate != datetime.date.fromordinal(1): + epStrings = [str(episode.airdate)] + else: + epStrings = ["S%02iE%02i" % (int(episode.season), int(episode.episode)), + "%ix%02i" % (int(episode.season), int(episode.episode))] + + # for single-season shows just search for the show name + if numseasons == 1: + epStrings = [''] + + showNames = set(makeSceneShowSearchStrings(episode.show)) + + toReturn = [] + + for curShow in showNames: + for curEpString in epStrings: + toReturn.append(curShow + '.' + curEpString) + + return toReturn + +def isGoodResult(name, show, log=True): + """ + Use an automatically-created regex to make sure the result actually is the show it claims to be + """ + + all_show_names = allPossibleShowNames(show) + showNames = map(sanitizeSceneName, all_show_names) + all_show_names + + for curName in set(showNames): + escaped_name = re.sub('\\\\[\\s.-]', '\W+', re.escape(curName)) + if show.startyear: + escaped_name += "(?:\W+"+str(show.startyear)+")?" + curRegex = '^' + escaped_name + '\W+(?:(?:S\d[\dE._ -])|(?:\d\d?x)|(?:\d{4}\W\d\d\W\d\d)|(?:(?:part|pt)[\._ -]?(\d|[ivx]))|Season\W+\d+\W+|E\d+\W+)' + if log: + logger.log(u"Checking if show "+name+" matches " + curRegex, logger.DEBUG) + + match = re.search(curRegex, name, re.I) + + if match: + logger.log(u"Matched "+curRegex+" to "+name, logger.DEBUG) + return True + + if log: + logger.log(u"Provider gave result "+name+" but that doesn't seem like a valid result for "+show.name+" so I'm ignoring it") + return False + +def allPossibleShowNames(show): + """ + Figures out every possible variation of the name for a particular show. Includes TVDB name, TVRage name, + country codes on the end, eg. "Show Name (AU)", and any scene exception names. + + show: a TVShow object that we should get the names of + + Returns: a list of all the possible show names + """ + + showNames = [show.name] + showNames += [name for name in get_scene_exceptions(show.tvdbid)] + + # if we have a tvrage name then use it + if show.tvrname != "" and show.tvrname != None: + showNames.append(show.tvrname) + + if show.custom_search_names != '': + for custom_name in show.custom_search_names.split(','): + showNames.append(custom_name) + + newShowNames = [] + + country_list = countryList + country_list.update(dict(zip(countryList.values(), countryList.keys()))) + + # if we have "Show Name Australia" or "Show Name (Australia)" this will add "Show Name (AU)" for + # any countries defined in common.countryList + # (and vice versa) + for curName in set(showNames): + if not curName: + continue + for curCountry in country_list: + if curName.endswith(' '+curCountry): + newShowNames.append(curName.replace(' '+curCountry, ' ('+country_list[curCountry]+')')) + elif curName.endswith(' ('+curCountry+')'): + newShowNames.append(curName.replace(' ('+curCountry+')', ' ('+country_list[curCountry]+')')) + + showNames += newShowNames + + return showNames + diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py index 4d7a28dbed194de1f1dbcb2fc4e3ac5862e22b0d..de486ece2ef6041d70830825e0175081a95e723b 100644 --- a/sickbeard/show_queue.py +++ b/sickbeard/show_queue.py @@ -1,452 +1,453 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import traceback - -import sickbeard - -from lib.tvdb_api import tvdb_exceptions, tvdb_api - -from sickbeard.common import SKIPPED, WANTED - -from sickbeard.tv import TVShow -from sickbeard import exceptions, logger, ui, db -from sickbeard import generic_queue -from sickbeard import name_cache -from sickbeard.exceptions import ex - -class ShowQueue(generic_queue.GenericQueue): - - def __init__(self): - generic_queue.GenericQueue.__init__(self) - self.queue_name = "SHOWQUEUE" - - - def _isInQueue(self, show, actions): - return show in [x.show for x in self.queue if x.action_id in actions] - - def _isBeingSomethinged(self, show, actions): - return self.currentItem != None and show == self.currentItem.show and \ - self.currentItem.action_id in actions - - def isInUpdateQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) - - def isInRefreshQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.REFRESH,)) - - def isInRenameQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.RENAME,)) - - def isBeingAdded(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.ADD,)) - - def isBeingUpdated(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) - - def isBeingRefreshed(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.REFRESH,)) - - def isBeingRenamed(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.RENAME,)) - - def _getLoadingShowList(self): - return [x for x in self.queue+[self.currentItem] if x != None and x.isLoading] - - loadingShowList = property(_getLoadingShowList) - - def updateShow(self, show, force=False): - - if self.isBeingAdded(show): - raise exceptions.CantUpdateException("Show is still being added, wait until it is finished before you update.") - - if self.isBeingUpdated(show): - raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") - - if self.isInUpdateQueue(show): - raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") - - if not force: - queueItemObj = QueueItemUpdate(show) - else: - queueItemObj = QueueItemForceUpdate(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def refreshShow(self, show, force=False): - - if self.isBeingRefreshed(show) and not force: - raise exceptions.CantRefreshException("This show is already being refreshed, not refreshing again.") - - if (self.isBeingUpdated(show) or self.isInUpdateQueue(show)) and not force: - logger.log(u"A refresh was attempted but there is already an update queued or in progress. Since updates do a refres at the end anyway I'm skipping this request.", logger.DEBUG) - return - - queueItemObj = QueueItemRefresh(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def renameShowEpisodes(self, show, force=False): - - queueItemObj = QueueItemRename(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def addShow(self, tvdb_id, showDir, default_status=None, quality=None, flatten_folders=None, lang="en"): - queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, flatten_folders, lang) - - self.add_item(queueItemObj) - - return queueItemObj - -class ShowQueueActions: - REFRESH=1 - ADD=2 - UPDATE=3 - FORCEUPDATE=4 - RENAME=5 - - names = {REFRESH: 'Refresh', - ADD: 'Add', - UPDATE: 'Update', - FORCEUPDATE: 'Force Update', - RENAME: 'Rename', - } - -class ShowQueueItem(generic_queue.QueueItem): - """ - Represents an item in the queue waiting to be executed - - Can be either: - - show being added (may or may not be associated with a show object) - - show being refreshed - - show being updated - - show being force updated - """ - def __init__(self, action_id, show): - generic_queue.QueueItem.__init__(self, ShowQueueActions.names[action_id], action_id) - self.show = show - - def isInQueue(self): - return self in sickbeard.showQueueScheduler.action.queue+[sickbeard.showQueueScheduler.action.currentItem] #@UndefinedVariable - - def _getName(self): - return str(self.show.tvdbid) - - def _isLoading(self): - return False - - show_name = property(_getName) - - isLoading = property(_isLoading) - - -class QueueItemAdd(ShowQueueItem): - def __init__(self, tvdb_id, showDir, default_status, quality, flatten_folders, lang): - - self.tvdb_id = tvdb_id - self.showDir = showDir - self.default_status = default_status - self.quality = quality - self.flatten_folders = flatten_folders - self.lang = lang - - self.show = None - - # this will initialize self.show to None - ShowQueueItem.__init__(self, ShowQueueActions.ADD, self.show) - - def _getName(self): - """ - Returns the show name if there is a show object created, if not returns - the dir that the show is being added to. - """ - if self.show == None: - return self.showDir - return self.show.name - - show_name = property(_getName) - - def _isLoading(self): - """ - Returns True if we've gotten far enough to have a show object, or False - if we still only know the folder name. - """ - if self.show == None: - return True - return False - - isLoading = property(_isLoading) - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Starting to add show "+self.showDir) - - try: - # make sure the tvdb ids are valid - try: - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - if self.lang: - ltvdb_api_parms['language'] = self.lang - - logger.log(u"TVDB: "+repr(ltvdb_api_parms)) - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - s = t[self.tvdb_id] - - # this usually only happens if they have an NFO in their show dir which gave us a TVDB ID that has no proper english version of the show - if not s['seriesname']: - logger.log(u"Show in " + self.showDir + " has no name on TVDB, probably the wrong language used to search with.", logger.ERROR) - ui.notifications.error("Unable to add show", "Show in " + self.showDir + " has no name on TVDB, probably the wrong language. Delete .nfo and add manually in the correct language.") - self._finishEarly() - return - # if the show has no episodes/seasons - if not s: - logger.log(u"Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.", logger.ERROR) - ui.notifications.error("Unable to add show", "Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.") - self._finishEarly() - return - except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Error contacting TVDB: "+ex(e), logger.ERROR) - ui.notifications.error("Unable to add show", "Unable to look up the show in "+self.showDir+" on TVDB, not using the NFO. Delete .nfo and add manually in the correct language.") - self._finishEarly() - return - - # clear the name cache - name_cache.clearCache() - - newShow = TVShow(self.tvdb_id, self.lang) - newShow.loadFromTVDB() - - self.show = newShow - - # set up initial values - self.show.location = self.showDir - self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT - self.show.flatten_folders = self.flatten_folders if self.flatten_folders != None else sickbeard.FLATTEN_FOLDERS_DEFAULT - self.show.paused = False - - # be smartish about this - if self.show.genre and "talk show" in self.show.genre.lower(): - self.show.air_by_date = 1 - - except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to add show due to an error with TVDB: "+ex(e), logger.ERROR) - if self.show: - ui.notifications.error("Unable to add "+str(self.show.name)+" due to an error with TVDB") - else: - ui.notifications.error("Unable to add show due to an error with TVDB") - self._finishEarly() - return - - except exceptions.MultipleShowObjectsException: - logger.log(u"The show in " + self.showDir + " is already in your show list, skipping", logger.ERROR) - ui.notifications.error('Show skipped', "The show in " + self.showDir + " is already in your show list") - self._finishEarly() - return - - except Exception, e: - logger.log(u"Error trying to add show: "+ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - self._finishEarly() - raise - - # add it to the show list - sickbeard.showList.append(self.show) - - try: - self.show.loadEpisodesFromDir() - except Exception, e: - logger.log(u"Error searching dir for episodes: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - try: - self.show.loadEpisodesFromTVDB() - self.show.setTVRID() - - self.show.writeMetadata() - self.show.populateCache() - - except Exception, e: - logger.log(u"Error with TVDB, not creating episode list: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - try: - self.show.saveToDB() - except Exception, e: - logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - # if they gave a custom status then change all the eps to it - if self.default_status != SKIPPED: - logger.log(u"Setting all episodes to the specified default status: "+str(self.default_status)) - myDB = db.DBConnection(); - myDB.action("UPDATE tv_episodes SET status = ? WHERE status = ? AND showid = ? AND season != 0", [self.default_status, SKIPPED, self.show.tvdbid]) - - # if they started with WANTED eps then run the backlog - if self.default_status == WANTED: - logger.log(u"Launching backlog for this show since its episodes are WANTED") - sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) #@UndefinedVariable - - self.show.flushEpisodes() - - self.finish() - - def _finishEarly(self): - if self.show != None: - self.show.deleteShow() - - self.finish() - - -class QueueItemRefresh(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show) - - # do refreshes first because they're quick - self.priority = generic_queue.QueuePriorities.HIGH - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Performing refresh on "+self.show.name) - - self.show.refreshDir() - self.show.writeMetadata() - self.show.populateCache() - - self.inProgress = False - - -class QueueItemRename(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.RENAME, show) - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Performing rename on " + self.show.name) - - try: - show_loc = self.show.location - except exceptions.ShowDirNotFoundException: - logger.log(u"Can't perform rename on " + self.show.name + " when the show dir is missing.", logger.WARNING) - return - - ep_obj_rename_list = [] - - ep_obj_list = self.show.getAllEpisodes(has_location=True) - for cur_ep_obj in ep_obj_list: - # Only want to rename if we have a location - if cur_ep_obj.location: - if cur_ep_obj.relatedEps: - # do we have one of multi-episodes in the rename list already - have_already = False - for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: - if cur_related_ep in ep_obj_rename_list: - have_already = True - break - if not have_already: - ep_obj_rename_list.append(cur_ep_obj) - - else: - ep_obj_rename_list.append(cur_ep_obj) - - for cur_ep_obj in ep_obj_rename_list: - cur_ep_obj.rename() - - self.inProgress = False - - -class QueueItemUpdate(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.UPDATE, show) - self.force = False - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Beginning update of "+self.show.name) - - logger.log(u"Retrieving show info from TVDB", logger.DEBUG) - try: - self.show.loadFromTVDB(cache=not self.force) - except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to contact TVDB, aborting: "+ex(e), logger.WARNING) - return - - # get episode list from DB - logger.log(u"Loading all episodes from the database", logger.DEBUG) - DBEpList = self.show.loadEpisodesFromDB() - - # get episode list from TVDB - logger.log(u"Loading all episodes from theTVDB", logger.DEBUG) - try: - TVDBEpList = self.show.loadEpisodesFromTVDB(cache=not self.force) - except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to get info from TVDB, the show info will not be refreshed: "+ex(e), logger.ERROR) - TVDBEpList = None - - if TVDBEpList == None: - logger.log(u"No data returned from TVDB, unable to update this show", logger.ERROR) - - else: - - # for each ep we found on TVDB delete it from the DB list - for curSeason in TVDBEpList: - for curEpisode in TVDBEpList[curSeason]: - logger.log(u"Removing "+str(curSeason)+"x"+str(curEpisode)+" from the DB list", logger.DEBUG) - if curSeason in DBEpList and curEpisode in DBEpList[curSeason]: - del DBEpList[curSeason][curEpisode] - - # for the remaining episodes in the DB list just delete them from the DB - for curSeason in DBEpList: - for curEpisode in DBEpList[curSeason]: - logger.log(u"Permanently deleting episode "+str(curSeason)+"x"+str(curEpisode)+" from the database", logger.MESSAGE) - curEp = self.show.getEpisode(curSeason, curEpisode) - try: - curEp.deleteEpisode() - except exceptions.EpisodeDeletedException: - pass - - # now that we've updated the DB from TVDB see if there's anything we can add from TVRage - with self.show.lock: - logger.log(u"Attempting to supplement show info with info from TVRage", logger.DEBUG) - self.show.loadLatestFromTVRage() - if self.show.tvrid == 0: - self.show.setTVRID() - - sickbeard.showQueueScheduler.action.refreshShow(self.show, True) #@UndefinedVariable - -class QueueItemForceUpdate(QueueItemUpdate): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.FORCEUPDATE, show) - self.force = True +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import traceback + +import sickbeard + +from lib.tvdb_api import tvdb_exceptions, tvdb_api + +from sickbeard.common import SKIPPED, WANTED + +from sickbeard.tv import TVShow +from sickbeard import exceptions, logger, ui, db +from sickbeard import generic_queue +from sickbeard import name_cache +from sickbeard.exceptions import ex + +class ShowQueue(generic_queue.GenericQueue): + + def __init__(self): + generic_queue.GenericQueue.__init__(self) + self.queue_name = "SHOWQUEUE" + + + def _isInQueue(self, show, actions): + return show in [x.show for x in self.queue if x.action_id in actions] + + def _isBeingSomethinged(self, show, actions): + return self.currentItem != None and show == self.currentItem.show and \ + self.currentItem.action_id in actions + + def isInUpdateQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) + + def isInRefreshQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.REFRESH,)) + + def isInRenameQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.RENAME,)) + + def isBeingAdded(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.ADD,)) + + def isBeingUpdated(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) + + def isBeingRefreshed(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.REFRESH,)) + + def isBeingRenamed(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.RENAME,)) + + def _getLoadingShowList(self): + return [x for x in self.queue+[self.currentItem] if x != None and x.isLoading] + + loadingShowList = property(_getLoadingShowList) + + def updateShow(self, show, force=False): + + if self.isBeingAdded(show): + raise exceptions.CantUpdateException("Show is still being added, wait until it is finished before you update.") + + if self.isBeingUpdated(show): + raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") + + if self.isInUpdateQueue(show): + raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") + + if not force: + queueItemObj = QueueItemUpdate(show) + else: + queueItemObj = QueueItemForceUpdate(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def refreshShow(self, show, force=False): + + if self.isBeingRefreshed(show) and not force: + raise exceptions.CantRefreshException("This show is already being refreshed, not refreshing again.") + + if (self.isBeingUpdated(show) or self.isInUpdateQueue(show)) and not force: + logger.log(u"A refresh was attempted but there is already an update queued or in progress. Since updates do a refres at the end anyway I'm skipping this request.", logger.DEBUG) + return + + queueItemObj = QueueItemRefresh(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def renameShowEpisodes(self, show, force=False): + + queueItemObj = QueueItemRename(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def addShow(self, tvdb_id, showDir, default_status=None, quality=None, flatten_folders=None, lang="en",audio_lang="en"): + queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, flatten_folders, lang,audio_lang) + + self.add_item(queueItemObj) + + return queueItemObj + +class ShowQueueActions: + REFRESH=1 + ADD=2 + UPDATE=3 + FORCEUPDATE=4 + RENAME=5 + + names = {REFRESH: 'Refresh', + ADD: 'Add', + UPDATE: 'Update', + FORCEUPDATE: 'Force Update', + RENAME: 'Rename', + } + +class ShowQueueItem(generic_queue.QueueItem): + """ + Represents an item in the queue waiting to be executed + + Can be either: + - show being added (may or may not be associated with a show object) + - show being refreshed + - show being updated + - show being force updated + """ + def __init__(self, action_id, show): + generic_queue.QueueItem.__init__(self, ShowQueueActions.names[action_id], action_id) + self.show = show + + def isInQueue(self): + return self in sickbeard.showQueueScheduler.action.queue+[sickbeard.showQueueScheduler.action.currentItem] #@UndefinedVariable + + def _getName(self): + return str(self.show.tvdbid) + + def _isLoading(self): + return False + + show_name = property(_getName) + + isLoading = property(_isLoading) + + +class QueueItemAdd(ShowQueueItem): + def __init__(self, tvdb_id, showDir, default_status, quality, flatten_folders, lang,audio_lang): + + self.tvdb_id = tvdb_id + self.showDir = showDir + self.default_status = default_status + self.quality = quality + self.flatten_folders = flatten_folders + self.lang = lang + self.audio_lang = audio_lang + + self.show = None + + # this will initialize self.show to None + ShowQueueItem.__init__(self, ShowQueueActions.ADD, self.show) + + def _getName(self): + """ + Returns the show name if there is a show object created, if not returns + the dir that the show is being added to. + """ + if self.show == None: + return self.showDir + return self.show.name + + show_name = property(_getName) + + def _isLoading(self): + """ + Returns True if we've gotten far enough to have a show object, or False + if we still only know the folder name. + """ + if self.show == None: + return True + return False + + isLoading = property(_isLoading) + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Starting to add show "+self.showDir) + + try: + # make sure the tvdb ids are valid + try: + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + if self.lang: + ltvdb_api_parms['language'] = self.lang + + logger.log(u"TVDB: "+repr(ltvdb_api_parms)) + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + s = t[self.tvdb_id] + + # this usually only happens if they have an NFO in their show dir which gave us a TVDB ID that has no proper english version of the show + if not s['seriesname']: + logger.log(u"Show in " + self.showDir + " has no name on TVDB, probably the wrong language used to search with.", logger.ERROR) + ui.notifications.error("Unable to add show", "Show in " + self.showDir + " has no name on TVDB, probably the wrong language. Delete .nfo and add manually in the correct language.") + self._finishEarly() + return + # if the show has no episodes/seasons + if not s: + logger.log(u"Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.", logger.ERROR) + ui.notifications.error("Unable to add show", "Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.") + self._finishEarly() + return + except tvdb_exceptions.tvdb_exception, e: + logger.log(u"Error contacting TVDB: "+ex(e), logger.ERROR) + ui.notifications.error("Unable to add show", "Unable to look up the show in "+self.showDir+" on TVDB, not using the NFO. Delete .nfo and add manually in the correct language.") + self._finishEarly() + return + + # clear the name cache + name_cache.clearCache() + + newShow = TVShow(self.tvdb_id, self.lang, self.audio_lang) + newShow.loadFromTVDB() + + self.show = newShow + + # set up initial values + self.show.location = self.showDir + self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT + self.show.flatten_folders = self.flatten_folders if self.flatten_folders != None else sickbeard.FLATTEN_FOLDERS_DEFAULT + self.show.paused = False + + # be smartish about this + if self.show.genre and "talk show" in self.show.genre.lower(): + self.show.air_by_date = 1 + + except tvdb_exceptions.tvdb_exception, e: + logger.log(u"Unable to add show due to an error with TVDB: "+ex(e), logger.ERROR) + if self.show: + ui.notifications.error("Unable to add "+str(self.show.name)+" due to an error with TVDB") + else: + ui.notifications.error("Unable to add show due to an error with TVDB") + self._finishEarly() + return + + except exceptions.MultipleShowObjectsException: + logger.log(u"The show in " + self.showDir + " is already in your show list, skipping", logger.ERROR) + ui.notifications.error('Show skipped', "The show in " + self.showDir + " is already in your show list") + self._finishEarly() + return + + except Exception, e: + logger.log(u"Error trying to add show: "+ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + self._finishEarly() + raise + + # add it to the show list + sickbeard.showList.append(self.show) + + try: + self.show.loadEpisodesFromDir() + except Exception, e: + logger.log(u"Error searching dir for episodes: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + try: + self.show.loadEpisodesFromTVDB() + self.show.setTVRID() + + self.show.writeMetadata() + self.show.populateCache() + + except Exception, e: + logger.log(u"Error with TVDB, not creating episode list: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + try: + self.show.saveToDB() + except Exception, e: + logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + # if they gave a custom status then change all the eps to it + if self.default_status != SKIPPED: + logger.log(u"Setting all episodes to the specified default status: "+str(self.default_status)) + myDB = db.DBConnection(); + myDB.action("UPDATE tv_episodes SET status = ? WHERE status = ? AND showid = ? AND season != 0", [self.default_status, SKIPPED, self.show.tvdbid]) + + # if they started with WANTED eps then run the backlog + if self.default_status == WANTED: + logger.log(u"Launching backlog for this show since its episodes are WANTED") + sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) #@UndefinedVariable + + self.show.flushEpisodes() + + self.finish() + + def _finishEarly(self): + if self.show != None: + self.show.deleteShow() + + self.finish() + + +class QueueItemRefresh(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show) + + # do refreshes first because they're quick + self.priority = generic_queue.QueuePriorities.HIGH + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Performing refresh on "+self.show.name) + + self.show.refreshDir() + self.show.writeMetadata() + self.show.populateCache() + + self.inProgress = False + + +class QueueItemRename(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.RENAME, show) + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Performing rename on " + self.show.name) + + try: + show_loc = self.show.location + except exceptions.ShowDirNotFoundException: + logger.log(u"Can't perform rename on " + self.show.name + " when the show dir is missing.", logger.WARNING) + return + + ep_obj_rename_list = [] + + ep_obj_list = self.show.getAllEpisodes(has_location=True) + for cur_ep_obj in ep_obj_list: + # Only want to rename if we have a location + if cur_ep_obj.location: + if cur_ep_obj.relatedEps: + # do we have one of multi-episodes in the rename list already + have_already = False + for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: + if cur_related_ep in ep_obj_rename_list: + have_already = True + break + if not have_already: + ep_obj_rename_list.append(cur_ep_obj) + + else: + ep_obj_rename_list.append(cur_ep_obj) + + for cur_ep_obj in ep_obj_rename_list: + cur_ep_obj.rename() + + self.inProgress = False + + +class QueueItemUpdate(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.UPDATE, show) + self.force = False + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Beginning update of "+self.show.name) + + logger.log(u"Retrieving show info from TVDB", logger.DEBUG) + try: + self.show.loadFromTVDB(cache=not self.force) + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB, aborting: "+ex(e), logger.WARNING) + return + + # get episode list from DB + logger.log(u"Loading all episodes from the database", logger.DEBUG) + DBEpList = self.show.loadEpisodesFromDB() + + # get episode list from TVDB + logger.log(u"Loading all episodes from theTVDB", logger.DEBUG) + try: + TVDBEpList = self.show.loadEpisodesFromTVDB(cache=not self.force) + except tvdb_exceptions.tvdb_exception, e: + logger.log(u"Unable to get info from TVDB, the show info will not be refreshed: "+ex(e), logger.ERROR) + TVDBEpList = None + + if TVDBEpList == None: + logger.log(u"No data returned from TVDB, unable to update this show", logger.ERROR) + + else: + + # for each ep we found on TVDB delete it from the DB list + for curSeason in TVDBEpList: + for curEpisode in TVDBEpList[curSeason]: + logger.log(u"Removing "+str(curSeason)+"x"+str(curEpisode)+" from the DB list", logger.DEBUG) + if curSeason in DBEpList and curEpisode in DBEpList[curSeason]: + del DBEpList[curSeason][curEpisode] + + # for the remaining episodes in the DB list just delete them from the DB + for curSeason in DBEpList: + for curEpisode in DBEpList[curSeason]: + logger.log(u"Permanently deleting episode "+str(curSeason)+"x"+str(curEpisode)+" from the database", logger.MESSAGE) + curEp = self.show.getEpisode(curSeason, curEpisode) + try: + curEp.deleteEpisode() + except exceptions.EpisodeDeletedException: + pass + + # now that we've updated the DB from TVDB see if there's anything we can add from TVRage + with self.show.lock: + logger.log(u"Attempting to supplement show info with info from TVRage", logger.DEBUG) + self.show.loadLatestFromTVRage() + if self.show.tvrid == 0: + self.show.setTVRID() + + sickbeard.showQueueScheduler.action.refreshShow(self.show, True) #@UndefinedVariable + +class QueueItemForceUpdate(QueueItemUpdate): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.FORCEUPDATE, show) + self.force = True diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 1ebe598b0f4594a0ed8040193e2f06626f1c17cf..458243d9d27069a66b4c0965339cb7917f0fb8bd 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -1,1754 +1,1761 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import os.path -import datetime -import threading -import re -import glob - -import sickbeard - -import xml.etree.cElementTree as etree - -from name_parser.parser import NameParser, InvalidNameException - -from lib.tvdb_api import tvdb_api, tvdb_exceptions - -from sickbeard import db -from sickbeard import helpers, exceptions, logger -from sickbeard.exceptions import ex -from sickbeard import tvrage -from sickbeard import image_cache -from sickbeard import postProcessor - -from sickbeard import encodingKludge as ek - -from common import Quality, Overview -from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, ARCHIVED, IGNORED, UNAIRED, WANTED, SKIPPED, UNKNOWN -from common import NAMING_DUPLICATE, NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_SEPARATED_REPEAT, NAMING_LIMITED_EXTEND_E_PREFIXED - -class TVShow(object): - - def __init__ (self, tvdbid, lang=""): - - self.tvdbid = tvdbid - - self._location = "" - self.name = "" - self.tvrid = 0 - self.tvrname = "" - self.network = "" - self.genre = "" - self.runtime = 0 - self.quality = int(sickbeard.QUALITY_DEFAULT) - self.flatten_folders = int(sickbeard.FLATTEN_FOLDERS_DEFAULT) - - self.status = "" - self.airs = "" - self.startyear = 0 - self.paused = 0 - self.air_by_date = 0 - self.lang = lang - self.audio_lang = "" - self.custom_search_names = "" - - self.lock = threading.Lock() - self._isDirGood = False - - self.episodes = {} - - otherShow = helpers.findCertainShow(sickbeard.showList, self.tvdbid) - if otherShow != None: - raise exceptions.MultipleShowObjectsException("Can't create a show if it already exists") - - self.loadFromDB() - - self.saveToDB() - - def _getLocation(self): - # no dir check needed if missing show dirs are created during post-processing - if sickbeard.CREATE_MISSING_SHOW_DIRS: - return self._location - - if ek.ek(os.path.isdir, self._location): - return self._location - else: - raise exceptions.ShowDirNotFoundException("Show folder doesn't exist, you shouldn't be using it") - - if self._isDirGood: - return self._location - else: - raise exceptions.NoNFOException("Show folder doesn't exist, you shouldn't be using it") - - def _setLocation(self, newLocation): - logger.log(u"Setter sets location to " + newLocation, logger.DEBUG) - # Don't validate dir if user wants to add shows without creating a dir - if sickbeard.ADD_SHOWS_WO_DIR or ek.ek(os.path.isdir, newLocation): - self._location = newLocation - self._isDirGood = True - else: - raise exceptions.NoNFOException("Invalid folder for the show!") - - location = property(_getLocation, _setLocation) - - # delete references to anything that's not in the internal lists - def flushEpisodes(self): - - for curSeason in self.episodes: - for curEp in self.episodes[curSeason]: - myEp = self.episodes[curSeason][curEp] - self.episodes[curSeason][curEp] = None - del myEp - - def getAllEpisodes(self, season=None, has_location=False): - - myDB = db.DBConnection() - - sql_selection = "SELECT season, episode, " - - # subselection to detect multi-episodes early, share_location > 0 - sql_selection = sql_selection + " (SELECT COUNT (*) FROM tv_episodes WHERE showid = tve.showid AND season = tve.season AND location != '' AND location = tve.location AND episode != tve.episode) AS share_location " - - sql_selection = sql_selection + " FROM tv_episodes tve WHERE showid = " + str(self.tvdbid) - - if season is not None: - sql_selection = sql_selection + " AND season = " + str(season) - if has_location: - sql_selection = sql_selection + " AND location != '' " - - # need ORDER episode ASC to rename multi-episodes in order S01E01-02 - sql_selection = sql_selection + " ORDER BY season ASC, episode ASC" - - results = myDB.select(sql_selection) - - ep_list = [] - for cur_result in results: - cur_ep = self.getEpisode(int(cur_result["season"]), int(cur_result["episode"])) - if cur_ep: - if cur_ep.location: - # if there is a location, check if it's a multi-episode (share_location > 0) and put them in relatedEps - if cur_result["share_location"] > 0: - related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND location = ? AND episode != ? ORDER BY episode ASC", [self.tvdbid, cur_ep.season, cur_ep.location, cur_ep.episode]) - for cur_related_ep in related_eps_result: - related_ep = self.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) - if related_ep not in cur_ep.relatedEps: - cur_ep.relatedEps.append(related_ep) - ep_list.append(cur_ep) - - return ep_list - - - def getEpisode(self, season, episode, file=None, noCreate=False): - - #return TVEpisode(self, season, episode) - - if not season in self.episodes: - self.episodes[season] = {} - - ep = None - - if not episode in self.episodes[season] or self.episodes[season][episode] == None: - if noCreate: - return None - - logger.log(str(self.tvdbid) + ": An object for episode " + str(season) + "x" + str(episode) + " didn't exist in the cache, trying to create it", logger.DEBUG) - - if file != None: - ep = TVEpisode(self, season, episode, file) - else: - ep = TVEpisode(self, season, episode) - - if ep != None: - self.episodes[season][episode] = ep - - return self.episodes[season][episode] - - def writeShowNFO(self): - - result = False - - if not ek.ek(os.path.isdir, self._location): - logger.log(str(self.tvdbid) + u": Show dir doesn't exist, skipping NFO generation") - return False - - for cur_provider in sickbeard.metadata_provider_dict.values(): - result = cur_provider.create_show_metadata(self) or result - - return result - - def writeMetadata(self, show_only=False): - - if not ek.ek(os.path.isdir, self._location): - logger.log(str(self.tvdbid) + u": Show dir doesn't exist, skipping NFO generation") - return - - self.getImages() - - self.writeShowNFO() - - if not show_only: - self.writeEpisodeNFOs() - - def writeEpisodeNFOs (self): - - if not ek.ek(os.path.isdir, self._location): - logger.log(str(self.tvdbid) + ": Show dir doesn't exist, skipping NFO generation") - return - - logger.log(str(self.tvdbid) + ": Writing NFOs for all episodes") - - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != ''", [self.tvdbid]) - - for epResult in sqlResults: - logger.log(str(self.tvdbid) + ": Retrieving/creating episode " + str(epResult["season"]) + "x" + str(epResult["episode"]), logger.DEBUG) - curEp = self.getEpisode(epResult["season"], epResult["episode"]) - curEp.createMetaFiles() - - - # find all media files in the show folder and create episodes for as many as possible - def loadEpisodesFromDir (self): - - if not ek.ek(os.path.isdir, self._location): - logger.log(str(self.tvdbid) + ": Show dir doesn't exist, not loading episodes from disk") - return - - logger.log(str(self.tvdbid) + ": Loading all episodes from the show directory " + self._location) - - # get file list - mediaFiles = helpers.listMediaFiles(self._location) - - # create TVEpisodes from each media file (if possible) - for mediaFile in mediaFiles: - - curEpisode = None - - logger.log(str(self.tvdbid) + ": Creating episode from " + mediaFile, logger.DEBUG) - try: - curEpisode = self.makeEpFromFile(ek.ek(os.path.join, self._location, mediaFile)) - except (exceptions.ShowNotFoundException, exceptions.EpisodeNotFoundException), e: - logger.log(u"Episode "+mediaFile+" returned an exception: "+ex(e), logger.ERROR) - continue - except exceptions.EpisodeDeletedException: - logger.log(u"The episode deleted itself when I tried making an object for it", logger.DEBUG) - - if curEpisode is None: - continue - - # see if we should save the release name in the db - ep_file_name = ek.ek(os.path.basename, curEpisode.location) - ep_file_name = ek.ek(os.path.splitext, ep_file_name)[0] - - parse_result = None - try: - np = NameParser(False) - parse_result = np.parse(ep_file_name) - except InvalidNameException: - pass - - if not ' ' in ep_file_name and parse_result and parse_result.release_group: - logger.log(u"Name " + ep_file_name + " gave release group of " + parse_result.release_group + ", seems valid", logger.DEBUG) - curEpisode.release_name = ep_file_name - - # store the reference in the show - if curEpisode != None: - curEpisode.saveToDB() - - - def loadEpisodesFromDB(self): - - logger.log(u"Loading all episodes from the DB") - - myDB = db.DBConnection() - sql = "SELECT * FROM tv_episodes WHERE showid = ?" - sqlResults = myDB.select(sql, [self.tvdbid]) - - scannedEps = {} - - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if self.lang: - ltvdb_api_parms['language'] = self.lang - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - - cachedShow = t[self.tvdbid] - cachedSeasons = {} - - for curResult in sqlResults: - - deleteEp = False - - curSeason = int(curResult["season"]) - curEpisode = int(curResult["episode"]) - if curSeason not in cachedSeasons: - try: - cachedSeasons[curSeason] = cachedShow[curSeason] - except tvdb_exceptions.tvdb_seasonnotfound, e: - logger.log(u"Error when trying to load the episode from TVDB: "+e.message, logger.WARNING) - deleteEp = True - - if not curSeason in scannedEps: - scannedEps[curSeason] = {} - - logger.log(u"Loading episode "+str(curSeason)+"x"+str(curEpisode)+" from the DB", logger.DEBUG) - - try: - curEp = self.getEpisode(curSeason, curEpisode) - - # if we found out that the ep is no longer on TVDB then delete it from our database too - if deleteEp: - curEp.deleteEpisode() - - curEp.loadFromDB(curSeason, curEpisode) - curEp.loadFromTVDB(tvapi=t, cachedSeason=cachedSeasons[curSeason]) - scannedEps[curSeason][curEpisode] = True - except exceptions.EpisodeDeletedException: - logger.log(u"Tried loading an episode from the DB that should have been deleted, skipping it", logger.DEBUG) - continue - - return scannedEps - - - def loadEpisodesFromTVDB(self, cache=True): - - # There's gotta be a better way of doing this but we don't wanna - # change the cache value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if not cache: - ltvdb_api_parms['cache'] = False - - if self.lang: - ltvdb_api_parms['language'] = self.lang - - try: - t = tvdb_api.Tvdb(**ltvdb_api_parms) - showObj = t[self.tvdbid] - except tvdb_exceptions.tvdb_error: - logger.log(u"TVDB timed out, unable to update episodes from TVDB", logger.ERROR) - return None - - logger.log(str(self.tvdbid) + ": Loading all episodes from theTVDB...") - - scannedEps = {} - - for season in showObj: - scannedEps[season] = {} - for episode in showObj[season]: - # need some examples of wtf episode 0 means to decide if we want it or not - if episode == 0: - continue - try: - #ep = TVEpisode(self, season, episode) - ep = self.getEpisode(season, episode) - except exceptions.EpisodeNotFoundException: - logger.log(str(self.tvdbid) + ": TVDB object for " + str(season) + "x" + str(episode) + " is incomplete, skipping this episode") - continue - else: - try: - ep.loadFromTVDB(tvapi=t) - except exceptions.EpisodeDeletedException: - logger.log(u"The episode was deleted, skipping the rest of the load") - continue - - with ep.lock: - logger.log(str(self.tvdbid) + ": Loading info from theTVDB for episode " + str(season) + "x" + str(episode), logger.DEBUG) - ep.loadFromTVDB(season, episode, tvapi=t) - if ep.dirty: - ep.saveToDB() - - scannedEps[season][episode] = True - - return scannedEps - - def setTVRID(self, force=False): - - if self.tvrid != 0 and not force: - logger.log(u"No need to get the TVRage ID, it's already populated", logger.DEBUG) - return - - logger.log(u"Attempting to retrieve the TVRage ID", logger.DEBUG) - - try: - # load the tvrage object, it will set the ID in its constructor if possible - tvrage.TVRage(self) - self.saveToDB() - except exceptions.TVRageException, e: - logger.log(u"Couldn't get TVRage ID because we're unable to sync TVDB and TVRage: "+ex(e), logger.DEBUG) - return - - def getImages(self, fanart=None, poster=None): - - poster_result = fanart_result = season_thumb_result = False - - for cur_provider in sickbeard.metadata_provider_dict.values(): - logger.log("Running season folders for "+cur_provider.name, logger.DEBUG) - poster_result = cur_provider.create_poster(self) or poster_result - fanart_result = cur_provider.create_fanart(self) or fanart_result - season_thumb_result = cur_provider.create_season_thumbs(self) or season_thumb_result - - return poster_result or fanart_result or season_thumb_result - - def loadLatestFromTVRage(self): - - try: - # load the tvrage object - tvr = tvrage.TVRage(self) - - newEp = tvr.findLatestEp() - - if newEp != None: - logger.log(u"TVRage gave us an episode object - saving it for now", logger.DEBUG) - newEp.saveToDB() - - # make an episode out of it - except exceptions.TVRageException, e: - logger.log(u"Unable to add TVRage info: " + ex(e), logger.WARNING) - - - - # make a TVEpisode object from a media file - def makeEpFromFile(self, file): - - if not ek.ek(os.path.isfile, file): - logger.log(str(self.tvdbid) + ": That isn't even a real file dude... " + file) - return None - - logger.log(str(self.tvdbid) + ": Creating episode object from " + file, logger.DEBUG) - - try: - myParser = NameParser() - parse_result = myParser.parse(file) - except InvalidNameException: - logger.log(u"Unable to parse the filename "+file+" into a valid episode", logger.ERROR) - return None - - if len(parse_result.episode_numbers) == 0 and not parse_result.air_by_date: - logger.log("parse_result: "+str(parse_result)) - logger.log(u"No episode number found in "+file+", ignoring it", logger.ERROR) - return None - - # for now lets assume that any episode in the show dir belongs to that show - season = parse_result.season_number if parse_result.season_number != None else 1 - episodes = parse_result.episode_numbers - rootEp = None - - # if we have an air-by-date show then get the real season/episode numbers - if parse_result.air_by_date: - try: - # There's gotta be a better way of doing this but we don't wanna - # change the cache value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if self.lang: - ltvdb_api_parms['language'] = self.lang - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - - epObj = t[self.tvdbid].airedOn(parse_result.air_date)[0] - season = int(epObj["seasonnumber"]) - episodes = [int(epObj["episodenumber"])] - except tvdb_exceptions.tvdb_episodenotfound: - logger.log(u"Unable to find episode with date " + str(parse_result.air_date) + " for show " + self.name + ", skipping", logger.WARNING) - return None - except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to contact TVDB: "+ex(e), logger.WARNING) - return None - - for curEpNum in episodes: - - episode = int(curEpNum) - - logger.log(str(self.tvdbid) + ": " + file + " parsed to " + self.name + " " + str(season) + "x" + str(episode), logger.DEBUG) - - checkQualityAgain = False - same_file = False - curEp = self.getEpisode(season, episode) - - if curEp == None: - try: - curEp = self.getEpisode(season, episode, file) - except exceptions.EpisodeNotFoundException: - logger.log(str(self.tvdbid) + ": Unable to figure out what this file is, skipping", logger.ERROR) - continue - - else: - # if there is a new file associated with this ep then re-check the quality - if curEp.location and ek.ek(os.path.normpath, curEp.location) != ek.ek(os.path.normpath, file): - logger.log(u"The old episode had a different file associated with it, I will re-check the quality based on the new filename "+file, logger.DEBUG) - checkQualityAgain = True - - with curEp.lock: - old_size = curEp.file_size - curEp.location = file - # if the sizes are the same then it's probably the same file - if old_size and curEp.file_size == old_size: - same_file = True - else: - same_file = False - - curEp.checkForMetaFiles() - - - if rootEp == None: - rootEp = curEp - else: - if curEp not in rootEp.relatedEps: - rootEp.relatedEps.append(curEp) - - # if it's a new file then - if not same_file: - curEp.release_name = '' - - # if they replace a file on me I'll make some attempt at re-checking the quality unless I know it's the same file - if checkQualityAgain and not same_file: - newQuality = Quality.nameQuality(file) - logger.log(u"Since this file has been renamed, I checked "+file+" and found quality "+Quality.qualityStrings[newQuality], logger.DEBUG) - if newQuality != Quality.UNKNOWN: - curEp.status = Quality.compositeStatus(DOWNLOADED, newQuality) - - - # check for status/quality changes as long as it's a new file - elif not same_file and sickbeard.helpers.isMediaFile(file) and curEp.status not in Quality.DOWNLOADED + [ARCHIVED, IGNORED]: - - oldStatus, oldQuality = Quality.splitCompositeStatus(curEp.status) - newQuality = Quality.nameQuality(file) - if newQuality == Quality.UNKNOWN: - newQuality = Quality.assumeQuality(file) - - newStatus = None - - # if it was snatched and now exists then set the status correctly - if oldStatus == SNATCHED and oldQuality <= newQuality: - logger.log(u"STATUS: this ep used to be snatched with quality "+Quality.qualityStrings[oldQuality]+" but a file exists with quality "+Quality.qualityStrings[newQuality]+" so I'm setting the status to DOWNLOADED", logger.DEBUG) - newStatus = DOWNLOADED - - # if it was snatched proper and we found a higher quality one then allow the status change - elif oldStatus == SNATCHED_PROPER and oldQuality < newQuality: - logger.log(u"STATUS: this ep used to be snatched proper with quality "+Quality.qualityStrings[oldQuality]+" but a file exists with quality "+Quality.qualityStrings[newQuality]+" so I'm setting the status to DOWNLOADED", logger.DEBUG) - newStatus = DOWNLOADED - - elif oldStatus not in (SNATCHED, SNATCHED_PROPER): - newStatus = DOWNLOADED - - if newStatus != None: - with curEp.lock: - logger.log(u"STATUS: we have an associated file, so setting the status from "+str(curEp.status)+" to DOWNLOADED/" + str(Quality.statusFromName(file)), logger.DEBUG) - curEp.status = Quality.compositeStatus(newStatus, newQuality) - - with curEp.lock: - curEp.saveToDB() - - # creating metafiles on the root should be good enough - if rootEp != None: - with rootEp.lock: - rootEp.createMetaFiles() - - return rootEp - - - def loadFromDB(self, skipNFO=False): - - logger.log(str(self.tvdbid) + ": Loading show info from database") - - myDB = db.DBConnection() - - sqlResults = myDB.select("SELECT * FROM tv_shows WHERE tvdb_id = ?", [self.tvdbid]) - - if len(sqlResults) > 1: - raise exceptions.MultipleDBShowsException() - elif len(sqlResults) == 0: - logger.log(str(self.tvdbid) + ": Unable to find the show in the database") - return - else: - if self.name == "": - self.name = sqlResults[0]["show_name"] - self.tvrname = sqlResults[0]["tvr_name"] - if self.network == "": - self.network = sqlResults[0]["network"] - if self.genre == "": - self.genre = sqlResults[0]["genre"] - - self.runtime = sqlResults[0]["runtime"] - - self.status = sqlResults[0]["status"] - if self.status == None: - self.status = "" - self.airs = sqlResults[0]["airs"] - if self.airs == None: - self.airs = "" - self.startyear = sqlResults[0]["startyear"] - if self.startyear == None: - self.startyear = 0 - - self.air_by_date = sqlResults[0]["air_by_date"] - if self.air_by_date == None: - self.air_by_date = 0 - - self.quality = int(sqlResults[0]["quality"]) - self.flatten_folders = int(sqlResults[0]["flatten_folders"]) - self.paused = int(sqlResults[0]["paused"]) - - self._location = sqlResults[0]["location"] - - if self.tvrid == 0: - self.tvrid = int(sqlResults[0]["tvr_id"]) - - if self.lang == "": - self.lang = sqlResults[0]["lang"] - - if self.audio_lang == "": - self.audio_lang = sqlResults[0]["audio_lang"] - - if self.custom_search_names == "": - self.custom_search_names = sqlResults[0]["custom_search_names"] - - def loadFromTVDB(self, cache=True, tvapi=None, cachedSeason=None): - - logger.log(str(self.tvdbid) + ": Loading show info from theTVDB") - - # There's gotta be a better way of doing this but we don't wanna - # change the cache value elsewhere - if tvapi is None: - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if not cache: - ltvdb_api_parms['cache'] = False - - if self.lang: - ltvdb_api_parms['language'] = self.lang - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - - else: - t = tvapi - - myEp = t[self.tvdbid] - - self.name = myEp["seriesname"] - - self.genre = myEp['genre'] - self.network = myEp['network'] - - if myEp["airs_dayofweek"] != None and myEp["airs_time"] != None: - self.airs = myEp["airs_dayofweek"] + " " + myEp["airs_time"] - - if myEp["firstaired"] != None and myEp["firstaired"]: - self.startyear = int(myEp["firstaired"].split('-')[0]) - - if self.airs == None: - self.airs = "" - - if myEp["status"] != None: - self.status = myEp["status"] - - if self.status == None: - self.status = "" - - self.saveToDB() - - - def loadNFO (self): - - if not os.path.isdir(self._location): - logger.log(str(self.tvdbid) + ": Show dir doesn't exist, can't load NFO") - raise exceptions.NoNFOException("The show dir doesn't exist, no NFO could be loaded") - - logger.log(str(self.tvdbid) + ": Loading show info from NFO") - - xmlFile = os.path.join(self._location, "tvshow.nfo") - - try: - xmlFileObj = open(xmlFile, 'r') - showXML = etree.ElementTree(file = xmlFileObj) - - if showXML.findtext('title') == None or (showXML.findtext('tvdbid') == None and showXML.findtext('id') == None): - raise exceptions.NoNFOException("Invalid info in tvshow.nfo (missing name or id):" \ - + str(showXML.findtext('title')) + " " \ - + str(showXML.findtext('tvdbid')) + " " \ - + str(showXML.findtext('id'))) - - self.name = showXML.findtext('title') - if showXML.findtext('tvdbid') != None: - self.tvdbid = int(showXML.findtext('tvdbid')) - elif showXML.findtext('id'): - self.tvdbid = int(showXML.findtext('id')) - else: - raise exceptions.NoNFOException("Empty <id> or <tvdbid> field in NFO") - - except (exceptions.NoNFOException, SyntaxError, ValueError), e: - logger.log(u"There was an error parsing your existing tvshow.nfo file: " + ex(e), logger.ERROR) - logger.log(u"Attempting to rename it to tvshow.nfo.old", logger.DEBUG) - - try: - xmlFileObj.close() - ek.ek(os.rename, xmlFile, xmlFile + ".old") - except Exception, e: - logger.log(u"Failed to rename your tvshow.nfo file - you need to delete it or fix it: " + ex(e), logger.ERROR) - raise exceptions.NoNFOException("Invalid info in tvshow.nfo") - - if showXML.findtext('studio') != None: - self.network = showXML.findtext('studio') - if self.network == None and showXML.findtext('network') != None: - self.network = "" - if showXML.findtext('genre') != None: - self.genre = showXML.findtext('genre') - else: - self.genre = "" - - # TODO: need to validate the input, I'm assuming it's good until then - - - def nextEpisode(self): - - logger.log(str(self.tvdbid) + ": Finding the episode which airs next", logger.DEBUG) - - myDB = db.DBConnection() - innerQuery = "SELECT airdate FROM tv_episodes WHERE showid = ? AND airdate >= ? AND status = ? ORDER BY airdate ASC LIMIT 1" - innerParams = [self.tvdbid, datetime.date.today().toordinal(), UNAIRED] - query = "SELECT * FROM tv_episodes WHERE showid = ? AND airdate >= ? AND airdate <= (" + innerQuery + ") and status = ?" - params = [self.tvdbid, datetime.date.today().toordinal()] + innerParams + [UNAIRED] - sqlResults = myDB.select(query, params) - - if sqlResults == None or len(sqlResults) == 0: - logger.log(str(self.tvdbid) + ": No episode found... need to implement tvrage and also show status", logger.DEBUG) - return [] - else: - logger.log(str(self.tvdbid) + ": Found episode " + str(sqlResults[0]["season"]) + "x" + str(sqlResults[0]["episode"]), logger.DEBUG) - foundEps = [] - for sqlEp in sqlResults: - curEp = self.getEpisode(int(sqlEp["season"]), int(sqlEp["episode"])) - foundEps.append(curEp) - return foundEps - - # if we didn't get an episode then try getting one from tvrage - - # load tvrage info - - # extract NextEpisode info - - # verify that we don't have it in the DB somehow (ep mismatch) - - - def deleteShow(self): - - myDB = db.DBConnection() - myDB.action("DELETE FROM tv_episodes WHERE showid = ?", [self.tvdbid]) - myDB.action("DELETE FROM tv_shows WHERE tvdb_id = ?", [self.tvdbid]) - - # remove self from show list - sickbeard.showList = [x for x in sickbeard.showList if x.tvdbid != self.tvdbid] - - # clear the cache - image_cache_dir = ek.ek(os.path.join, sickbeard.CACHE_DIR, 'images') - for cache_file in ek.ek(glob.glob, ek.ek(os.path.join, image_cache_dir, str(self.tvdbid)+'.*')): - logger.log(u"Deleting cache file "+cache_file) - os.remove(cache_file) - - def populateCache(self): - cache_inst = image_cache.ImageCache() - - logger.log(u"Checking & filling cache for show "+self.name) - cache_inst.fill_cache(self) - - def refreshDir(self): - - # make sure the show dir is where we think it is unless dirs are created on the fly - if not ek.ek(os.path.isdir, self._location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: - return False - - # load from dir - self.loadEpisodesFromDir() - - # run through all locations from DB, check that they exist - logger.log(str(self.tvdbid) + ": Loading all episodes with a location from the database") - - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != ''", [self.tvdbid]) - - for ep in sqlResults: - curLoc = os.path.normpath(ep["location"]) - season = int(ep["season"]) - episode = int(ep["episode"]) - - try: - curEp = self.getEpisode(season, episode) - except exceptions.EpisodeDeletedException: - logger.log(u"The episode was deleted while we were refreshing it, moving on to the next one", logger.DEBUG) - continue - - # if the path doesn't exist or if it's not in our show dir - if not ek.ek(os.path.isfile, curLoc) or not os.path.normpath(curLoc).startswith(os.path.normpath(self.location)): - - with curEp.lock: - # if it used to have a file associated with it and it doesn't anymore then set it to IGNORED - if curEp.location and curEp.status in Quality.DOWNLOADED: - logger.log(str(self.tvdbid) + ": Location for " + str(season) + "x" + str(episode) + " doesn't exist, removing it and changing our status to IGNORED", logger.DEBUG) - curEp.status = IGNORED - curEp.location = '' - curEp.hasnfo = False - curEp.hastbn = False - curEp.release_name = '' - curEp.saveToDB() - - def saveToDB(self): - - logger.log(str(self.tvdbid) + ": Saving show info to database", logger.DEBUG) - - myDB = db.DBConnection() - - controlValueDict = {"tvdb_id": self.tvdbid} - newValueDict = {"show_name": self.name, - "tvr_id": self.tvrid, - "location": self._location, - "network": self.network, - "genre": self.genre, - "runtime": self.runtime, - "quality": self.quality, - "airs": self.airs, - "status": self.status, - "flatten_folders": self.flatten_folders, - "paused": self.paused, - "air_by_date": self.air_by_date, - "startyear": self.startyear, - "tvr_name": self.tvrname, - "lang": self.lang, - "audio_lang": self.audio_lang, - "custom_search_names": self.custom_search_names - } - - myDB.upsert("tv_shows", newValueDict, controlValueDict) - - - def __str__(self): - toReturn = "" - toReturn += "name: " + self.name + "\n" - toReturn += "location: " + self._location + "\n" - toReturn += "tvdbid: " + str(self.tvdbid) + "\n" - if self.network != None: - toReturn += "network: " + self.network + "\n" - if self.airs != None: - toReturn += "airs: " + self.airs + "\n" - if self.status != None: - toReturn += "status: " + self.status + "\n" - toReturn += "startyear: " + str(self.startyear) + "\n" - toReturn += "genre: " + self.genre + "\n" - toReturn += "runtime: " + str(self.runtime) + "\n" - toReturn += "quality: " + str(self.quality) + "\n" - return toReturn - - - def wantEpisode(self, season, episode, quality, manualSearch=False): - - logger.log(u"Checking if we want episode "+str(season)+"x"+str(episode)+" at quality "+Quality.qualityStrings[quality], logger.DEBUG) - - # if the quality isn't one we want under any circumstances then just say no - anyQualities, bestQualities = Quality.splitQuality(self.quality) - logger.log(u"any,best = "+str(anyQualities)+" "+str(bestQualities)+" and we are "+str(quality), logger.DEBUG) - - if quality not in anyQualities + bestQualities: - logger.log(u"I know for sure I don't want this episode, saying no", logger.DEBUG) - return False - - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT status FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", [self.tvdbid, season, episode]) - - if not sqlResults or not len(sqlResults): - logger.log(u"Unable to find the episode", logger.DEBUG) - return False - - epStatus = int(sqlResults[0]["status"]) - - logger.log(u"current episode status: "+str(epStatus), logger.DEBUG) - - # if we know we don't want it then just say no - if epStatus in (SKIPPED, IGNORED, ARCHIVED) and not manualSearch: - logger.log(u"Ep is skipped, not bothering", logger.DEBUG) - return False - - # if it's one of these then we want it as long as it's in our allowed initial qualities - if quality in anyQualities + bestQualities: - if epStatus in (WANTED, UNAIRED, SKIPPED): - logger.log(u"Ep is wanted/unaired/skipped, definitely get it", logger.DEBUG) - return True - elif manualSearch: - logger.log(u"Usually I would ignore this ep but because you forced the search I'm overriding the default and allowing the quality", logger.DEBUG) - return True - else: - logger.log(u"This quality looks like something we might want but I don't know for sure yet", logger.DEBUG) - - curStatus, curQuality = Quality.splitCompositeStatus(epStatus) - - # if we are re-downloading then we only want it if it's in our bestQualities list and better than what we have - if curStatus in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER and quality in bestQualities and quality > curQuality: - logger.log(u"We already have this ep but the new one is better quality, saying yes", logger.DEBUG) - return True - - logger.log(u"None of the conditions were met so I'm just saying no", logger.DEBUG) - return False - - - def getOverview(self, epStatus): - - if epStatus == WANTED: - return Overview.WANTED - elif epStatus in (UNAIRED, UNKNOWN): - return Overview.UNAIRED - elif epStatus in (SKIPPED, IGNORED): - return Overview.SKIPPED - elif epStatus == ARCHIVED: - return Overview.GOOD - elif epStatus in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER: - - anyQualities, bestQualities = Quality.splitQuality(self.quality) #@UnusedVariable - if bestQualities: - maxBestQuality = max(bestQualities) - else: - maxBestQuality = None - - epStatus, curQuality = Quality.splitCompositeStatus(epStatus) - - # if they don't want re-downloads then we call it good if they have anything - if maxBestQuality == None: - return Overview.GOOD - # if they have one but it's not the best they want then mark it as qual - elif curQuality < maxBestQuality: - return Overview.QUAL - # if it's >= maxBestQuality then it's good - else: - return Overview.GOOD - -def dirty_setter(attr_name): - def wrapper(self, val): - if getattr(self, attr_name) != val: - setattr(self, attr_name, val) - self.dirty = True - return wrapper - -class TVEpisode(object): - - def __init__(self, show, season, episode, file=""): - - self._name = "" - self._season = season - self._episode = episode - self._description = "" - self._airdate = datetime.date.fromordinal(1) - self._hasnfo = False - self._hastbn = False - self._status = UNKNOWN - self._tvdbid = 0 - self._file_size = 0 - self._release_name = '' - - # setting any of the above sets the dirty flag - self.dirty = True - - self.show = show - self._location = file - - self.lock = threading.Lock() - - self.specifyEpisode(self.season, self.episode) - - self.relatedEps = [] - - self.checkForMetaFiles() - - name = property(lambda self: self._name, dirty_setter("_name")) - season = property(lambda self: self._season, dirty_setter("_season")) - episode = property(lambda self: self._episode, dirty_setter("_episode")) - description = property(lambda self: self._description, dirty_setter("_description")) - airdate = property(lambda self: self._airdate, dirty_setter("_airdate")) - hasnfo = property(lambda self: self._hasnfo, dirty_setter("_hasnfo")) - hastbn = property(lambda self: self._hastbn, dirty_setter("_hastbn")) - status = property(lambda self: self._status, dirty_setter("_status")) - tvdbid = property(lambda self: self._tvdbid, dirty_setter("_tvdbid")) - #location = property(lambda self: self._location, dirty_setter("_location")) - file_size = property(lambda self: self._file_size, dirty_setter("_file_size")) - release_name = property(lambda self: self._release_name, dirty_setter("_release_name")) - - def _set_location(self, new_location): - logger.log(u"Setter sets location to " + new_location, logger.DEBUG) - - #self._location = newLocation - dirty_setter("_location")(self, new_location) - - if new_location and ek.ek(os.path.isfile, new_location): - self.file_size = ek.ek(os.path.getsize, new_location) - else: - self.file_size = 0 - - location = property(lambda self: self._location, _set_location) - - def checkForMetaFiles(self): - - oldhasnfo = self.hasnfo - oldhastbn = self.hastbn - - cur_nfo = False - cur_tbn = False - - # check for nfo and tbn - if ek.ek(os.path.isfile, self.location): - for cur_provider in sickbeard.metadata_provider_dict.values(): - if cur_provider.episode_metadata: - new_result = cur_provider._has_episode_metadata(self) - else: - new_result = False - cur_nfo = new_result or cur_nfo - - if cur_provider.episode_thumbnails: - new_result = cur_provider._has_episode_thumb(self) - else: - new_result = False - cur_tbn = new_result or cur_tbn - - self.hasnfo = cur_nfo - self.hastbn = cur_tbn - - # if either setting has changed return true, if not return false - return oldhasnfo != self.hasnfo or oldhastbn != self.hastbn - - def specifyEpisode(self, season, episode): - - sqlResult = self.loadFromDB(season, episode) - - if not sqlResult: - # only load from NFO if we didn't load from DB - if ek.ek(os.path.isfile, self.location): - try: - self.loadFromNFO(self.location) - except exceptions.NoNFOException: - logger.log(str(self.show.tvdbid) + ": There was an error loading the NFO for episode " + str(season) + "x" + str(episode), logger.ERROR) - pass - - # if we tried loading it from NFO and didn't find the NFO, use TVDB - if self.hasnfo == False: - try: - result = self.loadFromTVDB(season, episode) - except exceptions.EpisodeDeletedException: - result = False - - # if we failed SQL *and* NFO, TVDB then fail - if result == False: - raise exceptions.EpisodeNotFoundException("Couldn't find episode " + str(season) + "x" + str(episode)) - - # don't update if not needed - if self.dirty: - self.saveToDB() - - def loadFromDB(self, season, episode): - - logger.log(str(self.show.tvdbid) + ": Loading episode details from DB for episode " + str(season) + "x" + str(episode), logger.DEBUG) - - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", [self.show.tvdbid, season, episode]) - - if len(sqlResults) > 1: - raise exceptions.MultipleDBEpisodesException("Your DB has two records for the same show somehow.") - elif len(sqlResults) == 0: - logger.log(str(self.show.tvdbid) + ": Episode " + str(self.season) + "x" + str(self.episode) + " not found in the database", logger.DEBUG) - return False - else: - #NAMEIT logger.log(u"AAAAA from" + str(self.season)+"x"+str(self.episode) + " -" + self.name + " to " + str(sqlResults[0]["name"])) - if sqlResults[0]["name"] != None: - self.name = sqlResults[0]["name"] - self.season = season - self.episode = episode - self.description = sqlResults[0]["description"] - if self.description == None: - self.description = "" - self.airdate = datetime.date.fromordinal(int(sqlResults[0]["airdate"])) - #logger.log(u"1 Status changes from " + str(self.status) + " to " + str(sqlResults[0]["status"]), logger.DEBUG) - self.status = int(sqlResults[0]["status"]) - - # don't overwrite my location - if sqlResults[0]["location"] != "" and sqlResults[0]["location"] != None: - self.location = os.path.normpath(sqlResults[0]["location"]) - if sqlResults[0]["file_size"]: - self.file_size = int(sqlResults[0]["file_size"]) - else: - self.file_size = 0 - - self.tvdbid = int(sqlResults[0]["tvdbid"]) - - if sqlResults[0]["release_name"] != None: - self.release_name = sqlResults[0]["release_name"] - - self.dirty = False - return True - - def loadFromTVDB(self, season=None, episode=None, cache=True, tvapi=None, cachedSeason=None): - - if season == None: - season = self.season - if episode == None: - episode = self.episode - - logger.log(str(self.show.tvdbid) + ": Loading episode details from theTVDB for episode " + str(season) + "x" + str(episode), logger.DEBUG) - - tvdb_lang = self.show.lang - - try: - if cachedSeason is None: - if tvapi is None: - # There's gotta be a better way of doing this but we don't wanna - # change the cache value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if not cache: - ltvdb_api_parms['cache'] = False - - if tvdb_lang: - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - else: - t = tvapi - myEp = t[self.show.tvdbid][season][episode] - else: - myEp = cachedSeason[episode] - - except (tvdb_exceptions.tvdb_error, IOError), e: - logger.log(u"TVDB threw up an error: "+ex(e), logger.DEBUG) - # if the episode is already valid just log it, if not throw it up - if self.name: - logger.log(u"TVDB timed out but we have enough info from other sources, allowing the error", logger.DEBUG) - return - else: - logger.log(u"TVDB timed out, unable to create the episode", logger.ERROR) - return False - except (tvdb_exceptions.tvdb_episodenotfound, tvdb_exceptions.tvdb_seasonnotfound): - logger.log(u"Unable to find the episode on tvdb... has it been removed? Should I delete from db?", logger.DEBUG) - # if I'm no longer on TVDB but I once was then delete myself from the DB - if self.tvdbid != -1: - self.deleteEpisode() - return - - - if not myEp["firstaired"] or myEp["firstaired"] == "0000-00-00": - myEp["firstaired"] = str(datetime.date.fromordinal(1)) - - if myEp["episodename"] == None or myEp["episodename"] == "": - logger.log(u"This episode ("+self.show.name+" - "+str(season)+"x"+str(episode)+") has no name on TVDB") - # if I'm incomplete on TVDB but I once was complete then just delete myself from the DB for now - if self.tvdbid != -1: - self.deleteEpisode() - return False - - #NAMEIT logger.log(u"BBBBBBBB from " + str(self.season)+"x"+str(self.episode) + " -" +self.name+" to "+myEp["episodename"]) - self.name = myEp["episodename"] - self.season = season - self.episode = episode - tmp_description = myEp["overview"] - if tmp_description == None: - self.description = "" - else: - self.description = tmp_description - rawAirdate = [int(x) for x in myEp["firstaired"].split("-")] - try: - self.airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2]) - except ValueError: - logger.log(u"Malformed air date retrieved from TVDB ("+self.show.name+" - "+str(season)+"x"+str(episode)+")", logger.ERROR) - # if I'm incomplete on TVDB but I once was complete then just delete myself from the DB for now - if self.tvdbid != -1: - self.deleteEpisode() - return False - - #early conversion to int so that episode doesn't get marked dirty - self.tvdbid = int(myEp["id"]) - - #don't update show status if show dir is missing, unless missing show dirs are created during post-processing - if not ek.ek(os.path.isdir, self.show._location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: - logger.log(u"The show dir is missing, not bothering to change the episode statuses since it'd probably be invalid") - return - - logger.log(str(self.show.tvdbid) + ": Setting status for " + str(season) + "x" + str(episode) + " based on status " + str(self.status) + " and existence of " + self.location, logger.DEBUG) - - if not ek.ek(os.path.isfile, self.location): - - # if we don't have the file - if self.airdate >= datetime.date.today() and self.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER: - # and it hasn't aired yet set the status to UNAIRED - logger.log(u"Episode airs in the future, changing status from " + str(self.status) + " to " + str(UNAIRED), logger.DEBUG) - self.status = UNAIRED - # if there's no airdate then set it to skipped (and respect ignored) - elif self.airdate == datetime.date.fromordinal(1): - if self.status == IGNORED: - logger.log(u"Episode has no air date, but it's already marked as ignored", logger.DEBUG) - else: - logger.log(u"Episode has no air date, automatically marking it skipped", logger.DEBUG) - self.status = SKIPPED - # if we don't have the file and the airdate is in the past - else: - if self.status == UNAIRED: - self.status = WANTED - - # if we somehow are still UNKNOWN then just skip it - elif self.status == UNKNOWN: - self.status = SKIPPED - - else: - logger.log(u"Not touching status because we have no ep file, the airdate is in the past, and the status is "+str(self.status), logger.DEBUG) - - # if we have a media file then it's downloaded - elif sickbeard.helpers.isMediaFile(self.location): - # leave propers alone, you have to either post-process them or manually change them back - if self.status not in Quality.SNATCHED_PROPER + Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED]: - logger.log(u"5 Status changes from " + str(self.status) + " to " + str(Quality.statusFromName(self.location)), logger.DEBUG) - self.status = Quality.statusFromName(self.location) - - # shouldn't get here probably - else: - logger.log(u"6 Status changes from " + str(self.status) + " to " + str(UNKNOWN), logger.DEBUG) - self.status = UNKNOWN - - - # hasnfo, hastbn, status? - - - def loadFromNFO(self, location): - - if not os.path.isdir(self.show._location): - logger.log(str(self.show.tvdbid) + ": The show dir is missing, not bothering to try loading the episode NFO") - return - - logger.log(str(self.show.tvdbid) + ": Loading episode details from the NFO file associated with " + location, logger.DEBUG) - - self.location = location - - if self.location != "": - - if self.status == UNKNOWN: - if sickbeard.helpers.isMediaFile(self.location): - logger.log(u"7 Status changes from " + str(self.status) + " to " + str(Quality.statusFromName(self.location)), logger.DEBUG) - self.status = Quality.statusFromName(self.location) - - nfoFile = sickbeard.helpers.replaceExtension(self.location, "nfo") - logger.log(str(self.show.tvdbid) + ": Using NFO name " + nfoFile, logger.DEBUG) - - if ek.ek(os.path.isfile, nfoFile): - try: - showXML = etree.ElementTree(file = nfoFile) - except (SyntaxError, ValueError), e: - logger.log(u"Error loading the NFO, backing up the NFO and skipping for now: " + ex(e), logger.ERROR) #TODO: figure out what's wrong and fix it - try: - ek.ek(os.rename, nfoFile, nfoFile + ".old") - except Exception, e: - logger.log(u"Failed to rename your episode's NFO file - you need to delete it or fix it: " + ex(e), logger.ERROR) - raise exceptions.NoNFOException("Error in NFO format") - - for epDetails in showXML.getiterator('episodedetails'): - if epDetails.findtext('season') == None or int(epDetails.findtext('season')) != self.season or \ - epDetails.findtext('episode') == None or int(epDetails.findtext('episode')) != self.episode: - logger.log(str(self.show.tvdbid) + ": NFO has an <episodedetails> block for a different episode - wanted " + str(self.season) + "x" + str(self.episode) + " but got " + str(epDetails.findtext('season')) + "x" + str(epDetails.findtext('episode')), logger.DEBUG) - continue - - if epDetails.findtext('title') == None or epDetails.findtext('aired') == None: - raise exceptions.NoNFOException("Error in NFO format (missing episode title or airdate)") - - self.name = epDetails.findtext('title') - self.episode = int(epDetails.findtext('episode')) - self.season = int(epDetails.findtext('season')) - - self.description = epDetails.findtext('plot') - if self.description == None: - self.description = "" - - if epDetails.findtext('aired'): - rawAirdate = [int(x) for x in epDetails.findtext('aired').split("-")] - self.airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2]) - else: - self.airdate = datetime.date.fromordinal(1) - - self.hasnfo = True - else: - self.hasnfo = False - - if ek.ek(os.path.isfile, sickbeard.helpers.replaceExtension(nfoFile, "tbn")): - self.hastbn = True - else: - self.hastbn = False - - def __str__ (self): - - toReturn = "" - toReturn += str(self.show.name) + " - " + str(self.season) + "x" + str(self.episode) + " - " + str(self.name) + "\n" - toReturn += "location: " + str(self.location) + "\n" - toReturn += "description: " + str(self.description) + "\n" - toReturn += "airdate: " + str(self.airdate.toordinal()) + " (" + str(self.airdate) + ")\n" - toReturn += "hasnfo: " + str(self.hasnfo) + "\n" - toReturn += "hastbn: " + str(self.hastbn) + "\n" - toReturn += "status: " + str(self.status) + "\n" - return toReturn - - def createMetaFiles(self, force=False): - - if not ek.ek(os.path.isdir, self.show._location): - logger.log(str(self.show.tvdbid) + ": The show dir is missing, not bothering to try to create metadata") - return - - self.createNFO(force) - self.createThumbnail(force) - - if self.checkForMetaFiles(): - self.saveToDB() - - def createNFO(self, force=False): - - result = False - - for cur_provider in sickbeard.metadata_provider_dict.values(): - result = cur_provider.create_episode_metadata(self) or result - - return result - - def createThumbnail(self, force=False): - - result = False - - for cur_provider in sickbeard.metadata_provider_dict.values(): - result = cur_provider.create_episode_thumb(self) or result - - return result - - def deleteEpisode(self): - - logger.log(u"Deleting "+self.show.name+" "+str(self.season)+"x"+str(self.episode)+" from the DB", logger.DEBUG) - - # remove myself from the show dictionary - if self.show.getEpisode(self.season, self.episode, noCreate=True) == self: - logger.log(u"Removing myself from my show's list", logger.DEBUG) - del self.show.episodes[self.season][self.episode] - - # delete myself from the DB - logger.log(u"Deleting myself from the database", logger.DEBUG) - myDB = db.DBConnection() - sql = "DELETE FROM tv_episodes WHERE showid="+str(self.show.tvdbid)+" AND season="+str(self.season)+" AND episode="+str(self.episode) - myDB.action(sql) - - raise exceptions.EpisodeDeletedException() - - def saveToDB(self, forceSave=False): - """ - Saves this episode to the database if any of its data has been changed since the last save. - - forceSave: If True it will save to the database even if no data has been changed since the - last save (aka if the record is not dirty). - """ - - if not self.dirty and not forceSave: - logger.log(str(self.show.tvdbid) + ": Not saving episode to db - record is not dirty", logger.DEBUG) - return - - logger.log(str(self.show.tvdbid) + ": Saving episode details to database", logger.DEBUG) - - logger.log(u"STATUS IS " + str(self.status), logger.DEBUG) - - myDB = db.DBConnection() - newValueDict = {"tvdbid": self.tvdbid, - "name": self.name, - "description": self.description, - "airdate": self.airdate.toordinal(), - "hasnfo": self.hasnfo, - "hastbn": self.hastbn, - "status": self.status, - "location": self.location, - "file_size": self.file_size, - "release_name": self.release_name} - controlValueDict = {"showid": self.show.tvdbid, - "season": self.season, - "episode": self.episode} - - # use a custom update/insert method to get the data into the DB - myDB.upsert("tv_episodes", newValueDict, controlValueDict) - - def fullPath (self): - if self.location == None or self.location == "": - return None - else: - return ek.ek(os.path.join, self.show.location, self.location) - - def prettyName(self): - """ - Returns the name of this episode in a "pretty" human-readable format. Used for logging - and notifications and such. - - Returns: A string representing the episode's name and season/ep numbers - """ - - return self._format_pattern('%SN - %Sx%0E - %EN') - - def _ep_name(self): - """ - Returns the name of the episode to use during renaming. Combines the names of related episodes. - Eg. "Ep Name (1)" and "Ep Name (2)" becomes "Ep Name" - "Ep Name" and "Other Ep Name" becomes "Ep Name & Other Ep Name" - """ - - multiNameRegex = "(.*) \(\d\)" - - self.relatedEps = sorted(self.relatedEps, key=lambda x: x.episode) - - if len(self.relatedEps) == 0: - goodName = self.name - - else: - goodName = '' - - singleName = True - curGoodName = None - - for curName in [self.name] + [x.name for x in self.relatedEps]: - match = re.match(multiNameRegex, curName) - if not match: - singleName = False - break - - if curGoodName == None: - curGoodName = match.group(1) - elif curGoodName != match.group(1): - singleName = False - break - - if singleName: - goodName = curGoodName - else: - goodName = self.name - for relEp in self.relatedEps: - goodName += " & " + relEp.name - - return goodName - - def _replace_map(self): - """ - Generates a replacement map for this episode which maps all possible custom naming patterns to the correct - value for this episode. - - Returns: A dict with patterns as the keys and their replacement values as the values. - """ - - ep_name = self._ep_name() - - def dot(name): - return helpers.sanitizeSceneName(name) - - def us(name): - return re.sub('[ -]','_', name) - - def release_name(name): - if name and name.lower().endswith('.nzb'): - name = name.rpartition('.')[0] - return name - - def release_group(name): - if not name: - return '' - - np = NameParser(name) - - try: - parse_result = np.parse(name) - except InvalidNameException, e: - logger.log(u"Unable to get parse release_group: "+ex(e), logger.DEBUG) - return '' - - if not parse_result.release_group: - return '' - return parse_result.release_group - - epStatus, epQual = Quality.splitCompositeStatus(self.status) #@UnusedVariable - - return { - '%SN': self.show.name, - '%S.N': dot(self.show.name), - '%S_N': us(self.show.name), - '%EN': ep_name, - '%E.N': dot(ep_name), - '%E_N': us(ep_name), - '%QN': Quality.qualityStrings[epQual], - '%Q.N': dot(Quality.qualityStrings[epQual]), - '%Q_N': us(Quality.qualityStrings[epQual]), - '%S': str(self.season), - '%0S': '%02d' % self.season, - '%E': str(self.episode), - '%0E': '%02d' % self.episode, - '%RN': release_name(self.release_name), - '%RG': release_group(self.release_name), - '%AD': str(self.airdate).replace('-', ' '), - '%A.D': str(self.airdate).replace('-', '.'), - '%A_D': us(str(self.airdate)), - '%A-D': str(self.airdate), - '%Y': str(self.airdate.year), - '%M': str(self.airdate.month), - '%D': str(self.airdate.day), - '%0M': '%02d' % self.airdate.month, - '%0D': '%02d' % self.airdate.day, - } - - def _format_string(self, pattern, replace_map): - """ - Replaces all template strings with the correct value - """ - - result_name = pattern - - # do the replacements - for cur_replacement in sorted(replace_map.keys(), reverse=True): - result_name = result_name.replace(cur_replacement, helpers.sanitizeFileName(replace_map[cur_replacement])) - result_name = result_name.replace(cur_replacement.lower(), helpers.sanitizeFileName(replace_map[cur_replacement].lower())) - - return result_name - - def _format_pattern(self, pattern=None, multi=None): - """ - Manipulates an episode naming pattern and then fills the template in - """ - - if pattern == None: - pattern = sickbeard.NAMING_PATTERN - - if multi == None: - multi = sickbeard.NAMING_MULTI_EP - - replace_map = self._replace_map() - - result_name = pattern - - # if there's no release group then replace it with a reasonable facsimile - if not replace_map['%RN']: - if self.show.air_by_date: - result_name = result_name.replace('%RN', '%S.N.%A.D.%E.N-SiCKBEARD') - result_name = result_name.replace('%rn', '%s.n.%A.D.%e.n-sickbeard') - else: - result_name = result_name.replace('%RN', '%S.N.S%0SE%0E.%E.N-SiCKBEARD') - result_name = result_name.replace('%rn', '%s.n.s%0se%0e.%e.n-sickbeard') - - result_name = result_name.replace('%RG', 'SiCKBEARD') - result_name = result_name.replace('%rg', 'sickbeard') - logger.log(u"Episode has no release name, replacing it with a generic one: "+result_name, logger.DEBUG) - - # split off ep name part only - name_groups = re.split(r'[\\/]', result_name) - - # figure out the double-ep numbering style for each group, if applicable - for cur_name_group in name_groups: - - season_format = sep = ep_sep = ep_format = None - - season_ep_regex = ''' - (?P<pre_sep>[ _.-]*) - ((?:s(?:eason|eries)?\s*)?%0?S(?![._]?N)) - (.*?) - (%0?E(?![._]?N)) - (?P<post_sep>[ _.-]*) - ''' - ep_only_regex = '(E?%0?E(?![._]?N))' - - # try the normal way - season_ep_match = re.search(season_ep_regex, cur_name_group, re.I|re.X) - ep_only_match = re.search(ep_only_regex, cur_name_group, re.I|re.X) - - # if we have a season and episode then collect the necessary data - if season_ep_match: - season_format = season_ep_match.group(2) - ep_sep = season_ep_match.group(3) - ep_format = season_ep_match.group(4) - sep = season_ep_match.group('pre_sep') - if not sep: - sep = season_ep_match.group('post_sep') - if not sep: - sep = ' ' - - # force 2-3-4 format if they chose to extend - if multi in (NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_LIMITED_EXTEND_E_PREFIXED): - ep_sep = '-' - - regex_used = season_ep_regex - - # if there's no season then there's not much choice so we'll just force them to use 03-04-05 style - elif ep_only_match: - season_format = '' - ep_sep = '-' - ep_format = ep_only_match.group(1) - sep = '' - regex_used = ep_only_regex - - else: - continue - - # we need at least this much info to continue - if not ep_sep or not ep_format: - continue - - # start with the ep string, eg. E03 - ep_string = self._format_string(ep_format.upper(), replace_map) - for other_ep in self.relatedEps: - - # for limited extend we only append the last ep - if multi in (NAMING_LIMITED_EXTEND, NAMING_LIMITED_EXTEND_E_PREFIXED) and other_ep != self.relatedEps[-1]: - continue - - elif multi == NAMING_DUPLICATE: - # add " - S01" - ep_string += sep + season_format - - elif multi == NAMING_SEPARATED_REPEAT: - ep_string += sep - - # add "E04" - ep_string += ep_sep - - if multi == NAMING_LIMITED_EXTEND_E_PREFIXED: - ep_string += 'E' - - ep_string += other_ep._format_string(ep_format.upper(), other_ep._replace_map()) - - if season_ep_match: - regex_replacement = r'\g<pre_sep>\g<2>\g<3>' + ep_string + r'\g<post_sep>' - elif ep_only_match: - regex_replacement = ep_string - - # fill out the template for this piece and then insert this piece into the actual pattern - cur_name_group_result = re.sub('(?i)(?x)'+regex_used, regex_replacement, cur_name_group) - #cur_name_group_result = cur_name_group.replace(ep_format, ep_string) - #logger.log(u"found "+ep_format+" as the ep pattern using "+regex_used+" and replaced it with "+regex_replacement+" to result in "+cur_name_group_result+" from "+cur_name_group, logger.DEBUG) - result_name = result_name.replace(cur_name_group, cur_name_group_result) - - result_name = self._format_string(result_name, replace_map) - - logger.log(u"formatting pattern: "+pattern+" -> "+result_name, logger.DEBUG) - - - return result_name - - def proper_path(self): - """ - Figures out the path where this episode SHOULD live according to the renaming rules, relative from the show dir - """ - - result = self.formatted_filename() - - # if they want us to flatten it and we're allowed to flatten it then we will - if self.show.flatten_folders and not sickbeard.NAMING_FORCE_FOLDERS: - return result - - # if not we append the folder on and use that - else: - result = ek.ek(os.path.join, self.formatted_dir(), result) - - return result - - - def formatted_dir(self, pattern=None, multi=None): - """ - Just the folder name of the episode - """ - - if pattern == None: - # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep - if self.show.air_by_date and sickbeard.NAMING_CUSTOM_ABD and not self.relatedEps: - pattern = sickbeard.NAMING_ABD_PATTERN - else: - pattern = sickbeard.NAMING_PATTERN - - # split off the dirs only, if they exist - name_groups = re.split(r'[\\/]', pattern) - - if len(name_groups) == 1: - return '' - else: - return self._format_pattern(os.sep.join(name_groups[:-1]), multi) - - - def formatted_filename(self, pattern=None, multi=None): - """ - Just the filename of the episode, formatted based on the naming settings - """ - - if pattern == None: - # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep - if self.show.air_by_date and sickbeard.NAMING_CUSTOM_ABD and not self.relatedEps: - pattern = sickbeard.NAMING_ABD_PATTERN - else: - pattern = sickbeard.NAMING_PATTERN - - # split off the filename only, if they exist - name_groups = re.split(r'[\\/]', pattern) - - return self._format_pattern(name_groups[-1], multi) - - def rename(self): - """ - Renames an episode file and all related files to the location and filename as specified - in the naming settings. - """ - - if not ek.ek(os.path.isfile, self.location): - logger.log(u"Can't perform rename on " + self.location + " when it doesn't exist, skipping", logger.WARNING) - return - - proper_path = self.proper_path() - absolute_proper_path = ek.ek(os.path.join, self.show.location, proper_path) - absolute_current_path_no_ext, file_ext = os.path.splitext(self.location) - - current_path = absolute_current_path_no_ext - - if absolute_current_path_no_ext.startswith(self.show.location): - current_path = absolute_current_path_no_ext[len(self.show.location):] - - logger.log(u"Renaming/moving episode from the base path " + self.location + " to " + absolute_proper_path, logger.DEBUG) - - # if it's already named correctly then don't do anything - if proper_path == current_path: - logger.log(str(self.tvdbid) + ": File " + self.location + " is already named correctly, skipping", logger.DEBUG) - return - - related_files = postProcessor.PostProcessor(self.location)._list_associated_files(self.location) - logger.log(u"Files associated to " + self.location + ": " + str(related_files), logger.DEBUG) - - # move the ep file - result = helpers.rename_ep_file(self.location, absolute_proper_path) - - # move related files - for cur_related_file in related_files: - cur_result = helpers.rename_ep_file(cur_related_file, absolute_proper_path) - if cur_result == False: - logger.log(str(self.tvdbid) + ": Unable to rename file " + cur_related_file, logger.ERROR) - - # save the ep - with self.lock: - if result != False: - self.location = absolute_proper_path + file_ext - for relEp in self.relatedEps: - relEp.location = absolute_proper_path + file_ext - - # in case something changed with the metadata just do a quick check - for curEp in [self] + self.relatedEps: - curEp.checkForMetaFiles() - - # save any changes to the database - with self.lock: - self.saveToDB() - for relEp in self.relatedEps: - relEp.saveToDB() +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import os.path +import datetime +import threading +import re +import glob + +import sickbeard + +import xml.etree.cElementTree as etree + +from name_parser.parser import NameParser, InvalidNameException + +from lib.tvdb_api import tvdb_api, tvdb_exceptions + +from sickbeard import db +from sickbeard import helpers, exceptions, logger +from sickbeard.exceptions import ex +from sickbeard import tvrage +from sickbeard import image_cache +from sickbeard import postProcessor + +from sickbeard import encodingKludge as ek + +from common import Quality, Overview +from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, ARCHIVED, IGNORED, UNAIRED, WANTED, SKIPPED, UNKNOWN +from common import NAMING_DUPLICATE, NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_SEPARATED_REPEAT, NAMING_LIMITED_EXTEND_E_PREFIXED + +class TVShow(object): + + def __init__ (self, tvdbid, lang="",audio_lang=""): + + self.tvdbid = tvdbid + + self._location = "" + self.name = "" + self.tvrid = 0 + self.tvrname = "" + self.network = "" + self.genre = "" + self.runtime = 0 + self.quality = int(sickbeard.QUALITY_DEFAULT) + self.flatten_folders = int(sickbeard.FLATTEN_FOLDERS_DEFAULT) + + self.status = "" + self.airs = "" + self.startyear = 0 + self.paused = 0 + self.air_by_date = 0 + self.lang = lang + self.audio_lang = audio_lang + self.custom_search_names = "" + + self.lock = threading.Lock() + self._isDirGood = False + + self.episodes = {} + + otherShow = helpers.findCertainShow(sickbeard.showList, self.tvdbid) + if otherShow != None: + raise exceptions.MultipleShowObjectsException("Can't create a show if it already exists") + + self.loadFromDB() + + self.saveToDB() + + def _getLocation(self): + # no dir check needed if missing show dirs are created during post-processing + if sickbeard.CREATE_MISSING_SHOW_DIRS: + return self._location + + if ek.ek(os.path.isdir, self._location): + return self._location + else: + raise exceptions.ShowDirNotFoundException("Show folder doesn't exist, you shouldn't be using it") + + if self._isDirGood: + return self._location + else: + raise exceptions.NoNFOException("Show folder doesn't exist, you shouldn't be using it") + + def _setLocation(self, newLocation): + logger.log(u"Setter sets location to " + newLocation, logger.DEBUG) + # Don't validate dir if user wants to add shows without creating a dir + if sickbeard.ADD_SHOWS_WO_DIR or ek.ek(os.path.isdir, newLocation): + self._location = newLocation + self._isDirGood = True + else: + raise exceptions.NoNFOException("Invalid folder for the show!") + + location = property(_getLocation, _setLocation) + + # delete references to anything that's not in the internal lists + def flushEpisodes(self): + + for curSeason in self.episodes: + for curEp in self.episodes[curSeason]: + myEp = self.episodes[curSeason][curEp] + self.episodes[curSeason][curEp] = None + del myEp + + def getAllEpisodes(self, season=None, has_location=False): + + myDB = db.DBConnection() + + sql_selection = "SELECT season, episode, " + + # subselection to detect multi-episodes early, share_location > 0 + sql_selection = sql_selection + " (SELECT COUNT (*) FROM tv_episodes WHERE showid = tve.showid AND season = tve.season AND location != '' AND location = tve.location AND episode != tve.episode) AS share_location " + + sql_selection = sql_selection + " FROM tv_episodes tve WHERE showid = " + str(self.tvdbid) + + if season is not None: + sql_selection = sql_selection + " AND season = " + str(season) + if has_location: + sql_selection = sql_selection + " AND location != '' " + + # need ORDER episode ASC to rename multi-episodes in order S01E01-02 + sql_selection = sql_selection + " ORDER BY season ASC, episode ASC" + + results = myDB.select(sql_selection) + + ep_list = [] + for cur_result in results: + cur_ep = self.getEpisode(int(cur_result["season"]), int(cur_result["episode"])) + if cur_ep: + if cur_ep.location: + # if there is a location, check if it's a multi-episode (share_location > 0) and put them in relatedEps + if cur_result["share_location"] > 0: + related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND location = ? AND episode != ? ORDER BY episode ASC", [self.tvdbid, cur_ep.season, cur_ep.location, cur_ep.episode]) + for cur_related_ep in related_eps_result: + related_ep = self.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) + if related_ep not in cur_ep.relatedEps: + cur_ep.relatedEps.append(related_ep) + ep_list.append(cur_ep) + + return ep_list + + + def getEpisode(self, season, episode, file=None, noCreate=False): + + #return TVEpisode(self, season, episode) + + if not season in self.episodes: + self.episodes[season] = {} + + ep = None + + if not episode in self.episodes[season] or self.episodes[season][episode] == None: + if noCreate: + return None + + logger.log(str(self.tvdbid) + ": An object for episode " + str(season) + "x" + str(episode) + " didn't exist in the cache, trying to create it", logger.DEBUG) + + if file != None: + ep = TVEpisode(self, season, episode, file) + else: + ep = TVEpisode(self, season, episode) + + if ep != None: + self.episodes[season][episode] = ep + + return self.episodes[season][episode] + + def writeShowNFO(self): + + result = False + + if not ek.ek(os.path.isdir, self._location): + logger.log(str(self.tvdbid) + u": Show dir doesn't exist, skipping NFO generation") + return False + + for cur_provider in sickbeard.metadata_provider_dict.values(): + result = cur_provider.create_show_metadata(self) or result + + return result + + def writeMetadata(self, show_only=False): + + if not ek.ek(os.path.isdir, self._location): + logger.log(str(self.tvdbid) + u": Show dir doesn't exist, skipping NFO generation") + return + + self.getImages() + + self.writeShowNFO() + + if not show_only: + self.writeEpisodeNFOs() + + def writeEpisodeNFOs (self): + + if not ek.ek(os.path.isdir, self._location): + logger.log(str(self.tvdbid) + ": Show dir doesn't exist, skipping NFO generation") + return + + logger.log(str(self.tvdbid) + ": Writing NFOs for all episodes") + + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != ''", [self.tvdbid]) + + for epResult in sqlResults: + logger.log(str(self.tvdbid) + ": Retrieving/creating episode " + str(epResult["season"]) + "x" + str(epResult["episode"]), logger.DEBUG) + curEp = self.getEpisode(epResult["season"], epResult["episode"]) + curEp.createMetaFiles() + + + # find all media files in the show folder and create episodes for as many as possible + def loadEpisodesFromDir (self): + + if not ek.ek(os.path.isdir, self._location): + logger.log(str(self.tvdbid) + ": Show dir doesn't exist, not loading episodes from disk") + return + + logger.log(str(self.tvdbid) + ": Loading all episodes from the show directory " + self._location) + + # get file list + mediaFiles = helpers.listMediaFiles(self._location) + + # create TVEpisodes from each media file (if possible) + for mediaFile in mediaFiles: + + curEpisode = None + + logger.log(str(self.tvdbid) + ": Creating episode from " + mediaFile, logger.DEBUG) + try: + curEpisode = self.makeEpFromFile(ek.ek(os.path.join, self._location, mediaFile)) + except (exceptions.ShowNotFoundException, exceptions.EpisodeNotFoundException), e: + logger.log(u"Episode "+mediaFile+" returned an exception: "+ex(e), logger.ERROR) + continue + except exceptions.EpisodeDeletedException: + logger.log(u"The episode deleted itself when I tried making an object for it", logger.DEBUG) + + if curEpisode is None: + continue + + # see if we should save the release name in the db + ep_file_name = ek.ek(os.path.basename, curEpisode.location) + ep_file_name = ek.ek(os.path.splitext, ep_file_name)[0] + + parse_result = None + try: + np = NameParser(False) + parse_result = np.parse(ep_file_name) + except InvalidNameException: + pass + + if not ' ' in ep_file_name and parse_result and parse_result.release_group: + logger.log(u"Name " + ep_file_name + " gave release group of " + parse_result.release_group + ", seems valid", logger.DEBUG) + curEpisode.release_name = ep_file_name + + # store the reference in the show + if curEpisode != None: + curEpisode.saveToDB() + + + def loadEpisodesFromDB(self): + + logger.log(u"Loading all episodes from the DB") + + myDB = db.DBConnection() + sql = "SELECT * FROM tv_episodes WHERE showid = ?" + sqlResults = myDB.select(sql, [self.tvdbid]) + + scannedEps = {} + + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if self.lang: + ltvdb_api_parms['language'] = self.lang + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + + cachedShow = t[self.tvdbid] + cachedSeasons = {} + + for curResult in sqlResults: + + deleteEp = False + + curSeason = int(curResult["season"]) + curEpisode = int(curResult["episode"]) + if curSeason not in cachedSeasons: + try: + cachedSeasons[curSeason] = cachedShow[curSeason] + except tvdb_exceptions.tvdb_seasonnotfound, e: + logger.log(u"Error when trying to load the episode from TVDB: "+e.message, logger.WARNING) + deleteEp = True + + if not curSeason in scannedEps: + scannedEps[curSeason] = {} + + logger.log(u"Loading episode "+str(curSeason)+"x"+str(curEpisode)+" from the DB", logger.DEBUG) + + try: + curEp = self.getEpisode(curSeason, curEpisode) + + # if we found out that the ep is no longer on TVDB then delete it from our database too + if deleteEp: + curEp.deleteEpisode() + + curEp.loadFromDB(curSeason, curEpisode) + curEp.loadFromTVDB(tvapi=t, cachedSeason=cachedSeasons[curSeason]) + scannedEps[curSeason][curEpisode] = True + except exceptions.EpisodeDeletedException: + logger.log(u"Tried loading an episode from the DB that should have been deleted, skipping it", logger.DEBUG) + continue + + return scannedEps + + + def loadEpisodesFromTVDB(self, cache=True): + + # There's gotta be a better way of doing this but we don't wanna + # change the cache value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if not cache: + ltvdb_api_parms['cache'] = False + + if self.lang: + ltvdb_api_parms['language'] = self.lang + + try: + t = tvdb_api.Tvdb(**ltvdb_api_parms) + showObj = t[self.tvdbid] + except tvdb_exceptions.tvdb_error: + logger.log(u"TVDB timed out, unable to update episodes from TVDB", logger.ERROR) + return None + + logger.log(str(self.tvdbid) + ": Loading all episodes from theTVDB...") + + scannedEps = {} + + for season in showObj: + scannedEps[season] = {} + for episode in showObj[season]: + # need some examples of wtf episode 0 means to decide if we want it or not + if episode == 0: + continue + try: + #ep = TVEpisode(self, season, episode) + ep = self.getEpisode(season, episode) + except exceptions.EpisodeNotFoundException: + logger.log(str(self.tvdbid) + ": TVDB object for " + str(season) + "x" + str(episode) + " is incomplete, skipping this episode") + continue + else: + try: + ep.loadFromTVDB(tvapi=t) + except exceptions.EpisodeDeletedException: + logger.log(u"The episode was deleted, skipping the rest of the load") + continue + + with ep.lock: + logger.log(str(self.tvdbid) + ": Loading info from theTVDB for episode " + str(season) + "x" + str(episode), logger.DEBUG) + ep.loadFromTVDB(season, episode, tvapi=t) + if ep.dirty: + ep.saveToDB() + + scannedEps[season][episode] = True + + return scannedEps + + def setTVRID(self, force=False): + + if self.tvrid != 0 and not force: + logger.log(u"No need to get the TVRage ID, it's already populated", logger.DEBUG) + return + + logger.log(u"Attempting to retrieve the TVRage ID", logger.DEBUG) + + try: + # load the tvrage object, it will set the ID in its constructor if possible + tvrage.TVRage(self) + self.saveToDB() + except exceptions.TVRageException, e: + logger.log(u"Couldn't get TVRage ID because we're unable to sync TVDB and TVRage: "+ex(e), logger.DEBUG) + return + + def getImages(self, fanart=None, poster=None): + + poster_result = fanart_result = season_thumb_result = False + + for cur_provider in sickbeard.metadata_provider_dict.values(): + logger.log("Running season folders for "+cur_provider.name, logger.DEBUG) + poster_result = cur_provider.create_poster(self) or poster_result + fanart_result = cur_provider.create_fanart(self) or fanart_result + season_thumb_result = cur_provider.create_season_thumbs(self) or season_thumb_result + + return poster_result or fanart_result or season_thumb_result + + def loadLatestFromTVRage(self): + + try: + # load the tvrage object + tvr = tvrage.TVRage(self) + + newEp = tvr.findLatestEp() + + if newEp != None: + logger.log(u"TVRage gave us an episode object - saving it for now", logger.DEBUG) + newEp.saveToDB() + + # make an episode out of it + except exceptions.TVRageException, e: + logger.log(u"Unable to add TVRage info: " + ex(e), logger.WARNING) + + + + # make a TVEpisode object from a media file + def makeEpFromFile(self, file): + + if not ek.ek(os.path.isfile, file): + logger.log(str(self.tvdbid) + ": That isn't even a real file dude... " + file) + return None + + logger.log(str(self.tvdbid) + ": Creating episode object from " + file, logger.DEBUG) + + try: + myParser = NameParser() + parse_result = myParser.parse(file) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+file+" into a valid episode", logger.ERROR) + return None + + if len(parse_result.episode_numbers) == 0 and not parse_result.air_by_date: + logger.log("parse_result: "+str(parse_result)) + logger.log(u"No episode number found in "+file+", ignoring it", logger.ERROR) + return None + + # for now lets assume that any episode in the show dir belongs to that show + season = parse_result.season_number if parse_result.season_number != None else 1 + episodes = parse_result.episode_numbers + rootEp = None + + # if we have an air-by-date show then get the real season/episode numbers + if parse_result.air_by_date: + try: + # There's gotta be a better way of doing this but we don't wanna + # change the cache value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if self.lang: + ltvdb_api_parms['language'] = self.lang + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + + epObj = t[self.tvdbid].airedOn(parse_result.air_date)[0] + season = int(epObj["seasonnumber"]) + episodes = [int(epObj["episodenumber"])] + except tvdb_exceptions.tvdb_episodenotfound: + logger.log(u"Unable to find episode with date " + str(parse_result.air_date) + " for show " + self.name + ", skipping", logger.WARNING) + return None + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB: "+ex(e), logger.WARNING) + return None + + for curEpNum in episodes: + + episode = int(curEpNum) + + logger.log(str(self.tvdbid) + ": " + file + " parsed to " + self.name + " " + str(season) + "x" + str(episode), logger.DEBUG) + + checkQualityAgain = False + same_file = False + curEp = self.getEpisode(season, episode) + + if curEp == None: + try: + curEp = self.getEpisode(season, episode, file) + except exceptions.EpisodeNotFoundException: + logger.log(str(self.tvdbid) + ": Unable to figure out what this file is, skipping", logger.ERROR) + continue + + else: + # if there is a new file associated with this ep then re-check the quality + if curEp.location and ek.ek(os.path.normpath, curEp.location) != ek.ek(os.path.normpath, file): + logger.log(u"The old episode had a different file associated with it, I will re-check the quality based on the new filename "+file, logger.DEBUG) + checkQualityAgain = True + + with curEp.lock: + old_size = curEp.file_size + curEp.location = file + # if the sizes are the same then it's probably the same file + if old_size and curEp.file_size == old_size: + same_file = True + else: + same_file = False + + curEp.checkForMetaFiles() + + + if rootEp == None: + rootEp = curEp + else: + if curEp not in rootEp.relatedEps: + rootEp.relatedEps.append(curEp) + + # if it's a new file then + if not same_file: + curEp.release_name = '' + + # if they replace a file on me I'll make some attempt at re-checking the quality unless I know it's the same file + if checkQualityAgain and not same_file: + newQuality = Quality.nameQuality(file) + logger.log(u"Since this file has been renamed, I checked "+file+" and found quality "+Quality.qualityStrings[newQuality], logger.DEBUG) + if newQuality != Quality.UNKNOWN: + curEp.status = Quality.compositeStatus(DOWNLOADED, newQuality) + + + # check for status/quality changes as long as it's a new file + elif not same_file and sickbeard.helpers.isMediaFile(file) and curEp.status not in Quality.DOWNLOADED + [ARCHIVED, IGNORED]: + + oldStatus, oldQuality = Quality.splitCompositeStatus(curEp.status) + newQuality = Quality.nameQuality(file) + if newQuality == Quality.UNKNOWN: + newQuality = Quality.assumeQuality(file) + + newStatus = None + + # if it was snatched and now exists then set the status correctly + if oldStatus == SNATCHED and oldQuality <= newQuality: + logger.log(u"STATUS: this ep used to be snatched with quality "+Quality.qualityStrings[oldQuality]+" but a file exists with quality "+Quality.qualityStrings[newQuality]+" so I'm setting the status to DOWNLOADED", logger.DEBUG) + newStatus = DOWNLOADED + + # if it was snatched proper and we found a higher quality one then allow the status change + elif oldStatus == SNATCHED_PROPER and oldQuality < newQuality: + logger.log(u"STATUS: this ep used to be snatched proper with quality "+Quality.qualityStrings[oldQuality]+" but a file exists with quality "+Quality.qualityStrings[newQuality]+" so I'm setting the status to DOWNLOADED", logger.DEBUG) + newStatus = DOWNLOADED + + elif oldStatus not in (SNATCHED, SNATCHED_PROPER): + newStatus = DOWNLOADED + + if newStatus != None: + with curEp.lock: + logger.log(u"STATUS: we have an associated file, so setting the status from "+str(curEp.status)+" to DOWNLOADED/" + str(Quality.statusFromName(file)), logger.DEBUG) + curEp.status = Quality.compositeStatus(newStatus, newQuality) + + with curEp.lock: + curEp.saveToDB() + + # creating metafiles on the root should be good enough + if rootEp != None: + with rootEp.lock: + rootEp.createMetaFiles() + + return rootEp + + + def loadFromDB(self, skipNFO=False): + + logger.log(str(self.tvdbid) + ": Loading show info from database") + + myDB = db.DBConnection() + + sqlResults = myDB.select("SELECT * FROM tv_shows WHERE tvdb_id = ?", [self.tvdbid]) + + if len(sqlResults) > 1: + raise exceptions.MultipleDBShowsException() + elif len(sqlResults) == 0: + logger.log(str(self.tvdbid) + ": Unable to find the show in the database") + return + else: + if self.name == "": + self.name = sqlResults[0]["show_name"] + self.tvrname = sqlResults[0]["tvr_name"] + if self.network == "": + self.network = sqlResults[0]["network"] + if self.genre == "": + self.genre = sqlResults[0]["genre"] + + self.runtime = sqlResults[0]["runtime"] + + self.status = sqlResults[0]["status"] + if self.status == None: + self.status = "" + self.airs = sqlResults[0]["airs"] + if self.airs == None: + self.airs = "" + self.startyear = sqlResults[0]["startyear"] + if self.startyear == None: + self.startyear = 0 + + self.air_by_date = sqlResults[0]["air_by_date"] + if self.air_by_date == None: + self.air_by_date = 0 + + self.quality = int(sqlResults[0]["quality"]) + self.flatten_folders = int(sqlResults[0]["flatten_folders"]) + self.paused = int(sqlResults[0]["paused"]) + + self._location = sqlResults[0]["location"] + + if self.tvrid == 0: + self.tvrid = int(sqlResults[0]["tvr_id"]) + + if self.lang == "": + self.lang = sqlResults[0]["lang"] + + if self.audio_lang == "": + self.audio_lang = sqlResults[0]["audio_lang"] + + if self.custom_search_names == "": + self.custom_search_names = sqlResults[0]["custom_search_names"] + + def loadFromTVDB(self, cache=True, tvapi=None, cachedSeason=None): + + logger.log(str(self.tvdbid) + ": Loading show info from theTVDB") + + # There's gotta be a better way of doing this but we don't wanna + # change the cache value elsewhere + if tvapi is None: + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if not cache: + ltvdb_api_parms['cache'] = False + + if self.lang: + ltvdb_api_parms['language'] = self.lang + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + + else: + t = tvapi + + myEp = t[self.tvdbid] + + self.name = myEp["seriesname"] + + self.genre = myEp['genre'] + self.network = myEp['network'] + + if myEp["airs_dayofweek"] != None and myEp["airs_time"] != None: + self.airs = myEp["airs_dayofweek"] + " " + myEp["airs_time"] + + if myEp["firstaired"] != None and myEp["firstaired"]: + self.startyear = int(myEp["firstaired"].split('-')[0]) + + if self.airs == None: + self.airs = "" + + if myEp["status"] != None: + self.status = myEp["status"] + + if self.status == None: + self.status = "" + + self.saveToDB() + + + def loadNFO (self): + + if not os.path.isdir(self._location): + logger.log(str(self.tvdbid) + ": Show dir doesn't exist, can't load NFO") + raise exceptions.NoNFOException("The show dir doesn't exist, no NFO could be loaded") + + logger.log(str(self.tvdbid) + ": Loading show info from NFO") + + xmlFile = os.path.join(self._location, "tvshow.nfo") + + try: + xmlFileObj = open(xmlFile, 'r') + showXML = etree.ElementTree(file = xmlFileObj) + + if showXML.findtext('title') == None or (showXML.findtext('tvdbid') == None and showXML.findtext('id') == None): + raise exceptions.NoNFOException("Invalid info in tvshow.nfo (missing name or id):" \ + + str(showXML.findtext('title')) + " " \ + + str(showXML.findtext('tvdbid')) + " " \ + + str(showXML.findtext('id'))) + + self.name = showXML.findtext('title') + if showXML.findtext('tvdbid') != None: + self.tvdbid = int(showXML.findtext('tvdbid')) + elif showXML.findtext('id'): + self.tvdbid = int(showXML.findtext('id')) + else: + raise exceptions.NoNFOException("Empty <id> or <tvdbid> field in NFO") + + except (exceptions.NoNFOException, SyntaxError, ValueError), e: + logger.log(u"There was an error parsing your existing tvshow.nfo file: " + ex(e), logger.ERROR) + logger.log(u"Attempting to rename it to tvshow.nfo.old", logger.DEBUG) + + try: + xmlFileObj.close() + ek.ek(os.rename, xmlFile, xmlFile + ".old") + except Exception, e: + logger.log(u"Failed to rename your tvshow.nfo file - you need to delete it or fix it: " + ex(e), logger.ERROR) + raise exceptions.NoNFOException("Invalid info in tvshow.nfo") + + if showXML.findtext('studio') != None: + self.network = showXML.findtext('studio') + if self.network == None and showXML.findtext('network') != None: + self.network = "" + if showXML.findtext('genre') != None: + self.genre = showXML.findtext('genre') + else: + self.genre = "" + + # TODO: need to validate the input, I'm assuming it's good until then + + + def nextEpisode(self): + + logger.log(str(self.tvdbid) + ": Finding the episode which airs next", logger.DEBUG) + + myDB = db.DBConnection() + innerQuery = "SELECT airdate FROM tv_episodes WHERE showid = ? AND airdate >= ? AND status = ? ORDER BY airdate ASC LIMIT 1" + innerParams = [self.tvdbid, datetime.date.today().toordinal(), UNAIRED] + query = "SELECT * FROM tv_episodes WHERE showid = ? AND airdate >= ? AND airdate <= (" + innerQuery + ") and status = ?" + params = [self.tvdbid, datetime.date.today().toordinal()] + innerParams + [UNAIRED] + sqlResults = myDB.select(query, params) + + if sqlResults == None or len(sqlResults) == 0: + logger.log(str(self.tvdbid) + ": No episode found... need to implement tvrage and also show status", logger.DEBUG) + return [] + else: + logger.log(str(self.tvdbid) + ": Found episode " + str(sqlResults[0]["season"]) + "x" + str(sqlResults[0]["episode"]), logger.DEBUG) + foundEps = [] + for sqlEp in sqlResults: + curEp = self.getEpisode(int(sqlEp["season"]), int(sqlEp["episode"])) + foundEps.append(curEp) + return foundEps + + # if we didn't get an episode then try getting one from tvrage + + # load tvrage info + + # extract NextEpisode info + + # verify that we don't have it in the DB somehow (ep mismatch) + + + def deleteShow(self): + + myDB = db.DBConnection() + myDB.action("DELETE FROM tv_episodes WHERE showid = ?", [self.tvdbid]) + myDB.action("DELETE FROM tv_shows WHERE tvdb_id = ?", [self.tvdbid]) + + # remove self from show list + sickbeard.showList = [x for x in sickbeard.showList if x.tvdbid != self.tvdbid] + + # clear the cache + image_cache_dir = ek.ek(os.path.join, sickbeard.CACHE_DIR, 'images') + for cache_file in ek.ek(glob.glob, ek.ek(os.path.join, image_cache_dir, str(self.tvdbid)+'.*')): + logger.log(u"Deleting cache file "+cache_file) + os.remove(cache_file) + + def populateCache(self): + cache_inst = image_cache.ImageCache() + + logger.log(u"Checking & filling cache for show "+self.name) + cache_inst.fill_cache(self) + + def refreshDir(self): + + # make sure the show dir is where we think it is unless dirs are created on the fly + if not ek.ek(os.path.isdir, self._location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: + return False + + # load from dir + self.loadEpisodesFromDir() + + # run through all locations from DB, check that they exist + logger.log(str(self.tvdbid) + ": Loading all episodes with a location from the database") + + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != ''", [self.tvdbid]) + + for ep in sqlResults: + curLoc = os.path.normpath(ep["location"]) + season = int(ep["season"]) + episode = int(ep["episode"]) + + try: + curEp = self.getEpisode(season, episode) + except exceptions.EpisodeDeletedException: + logger.log(u"The episode was deleted while we were refreshing it, moving on to the next one", logger.DEBUG) + continue + + # if the path doesn't exist or if it's not in our show dir + if not ek.ek(os.path.isfile, curLoc) or not os.path.normpath(curLoc).startswith(os.path.normpath(self.location)): + + with curEp.lock: + # if it used to have a file associated with it and it doesn't anymore then set it to IGNORED + if curEp.location and curEp.status in Quality.DOWNLOADED: + logger.log(str(self.tvdbid) + ": Location for " + str(season) + "x" + str(episode) + " doesn't exist, removing it and changing our status to IGNORED", logger.DEBUG) + curEp.status = IGNORED + curEp.location = '' + curEp.hasnfo = False + curEp.hastbn = False + curEp.release_name = '' + curEp.saveToDB() + + def saveToDB(self): + + logger.log(str(self.tvdbid) + ": Saving show info to database", logger.DEBUG) + + myDB = db.DBConnection() + + controlValueDict = {"tvdb_id": self.tvdbid} + newValueDict = {"show_name": self.name, + "tvr_id": self.tvrid, + "location": self._location, + "network": self.network, + "genre": self.genre, + "runtime": self.runtime, + "quality": self.quality, + "airs": self.airs, + "status": self.status, + "flatten_folders": self.flatten_folders, + "paused": self.paused, + "air_by_date": self.air_by_date, + "startyear": self.startyear, + "tvr_name": self.tvrname, + "lang": self.lang, + "audio_lang": self.audio_lang, + "custom_search_names": self.custom_search_names + } + + myDB.upsert("tv_shows", newValueDict, controlValueDict) + + + def __str__(self): + toReturn = "" + toReturn += "name: " + self.name + "\n" + toReturn += "location: " + self._location + "\n" + toReturn += "tvdbid: " + str(self.tvdbid) + "\n" + if self.network != None: + toReturn += "network: " + self.network + "\n" + if self.airs != None: + toReturn += "airs: " + self.airs + "\n" + if self.status != None: + toReturn += "status: " + self.status + "\n" + toReturn += "startyear: " + str(self.startyear) + "\n" + toReturn += "genre: " + self.genre + "\n" + toReturn += "runtime: " + str(self.runtime) + "\n" + toReturn += "quality: " + str(self.quality) + "\n" + return toReturn + + + def wantEpisode(self, season, episode, quality, manualSearch=False): + + logger.log(u"Checking if we want episode "+str(season)+"x"+str(episode)+" at quality "+Quality.qualityStrings[quality], logger.DEBUG) + + # if the quality isn't one we want under any circumstances then just say no + anyQualities, bestQualities = Quality.splitQuality(self.quality) + logger.log(u"any,best = "+str(anyQualities)+" "+str(bestQualities)+" and we are "+str(quality), logger.DEBUG) + + if quality not in anyQualities + bestQualities: + logger.log(u"I know for sure I don't want this episode, saying no", logger.DEBUG) + return False + + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT status FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", [self.tvdbid, season, episode]) + + if not sqlResults or not len(sqlResults): + logger.log(u"Unable to find the episode", logger.DEBUG) + return False + + epStatus = int(sqlResults[0]["status"]) + + logger.log(u"current episode status: "+str(epStatus), logger.DEBUG) + + # if we know we don't want it then just say no + if epStatus in (SKIPPED, IGNORED, ARCHIVED) and not manualSearch: + logger.log(u"Ep is skipped, not bothering", logger.DEBUG) + return False + + # if it's one of these then we want it as long as it's in our allowed initial qualities + if quality in anyQualities + bestQualities: + if epStatus in (WANTED, UNAIRED, SKIPPED): + logger.log(u"Ep is wanted/unaired/skipped, definitely get it", logger.DEBUG) + return True + elif manualSearch: + logger.log(u"Usually I would ignore this ep but because you forced the search I'm overriding the default and allowing the quality", logger.DEBUG) + return True + else: + logger.log(u"This quality looks like something we might want but I don't know for sure yet", logger.DEBUG) + + curStatus, curQuality = Quality.splitCompositeStatus(epStatus) + + # if we are re-downloading then we only want it if it's in our bestQualities list and better than what we have + if curStatus in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER and quality in bestQualities and quality > curQuality: + logger.log(u"We already have this ep but the new one is better quality, saying yes", logger.DEBUG) + return True + + logger.log(u"None of the conditions were met so I'm just saying no", logger.DEBUG) + return False + + + def getOverview(self, epStatus): + + if epStatus == WANTED: + return Overview.WANTED + elif epStatus in (UNAIRED, UNKNOWN): + return Overview.UNAIRED + elif epStatus in (SKIPPED, IGNORED): + return Overview.SKIPPED + elif epStatus == ARCHIVED: + return Overview.GOOD + elif epStatus in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER: + + anyQualities, bestQualities = Quality.splitQuality(self.quality) #@UnusedVariable + if bestQualities: + maxBestQuality = max(bestQualities) + else: + maxBestQuality = None + + epStatus, curQuality = Quality.splitCompositeStatus(epStatus) + + # if they don't want re-downloads then we call it good if they have anything + if maxBestQuality == None: + return Overview.GOOD + # if they have one but it's not the best they want then mark it as qual + elif curQuality < maxBestQuality: + return Overview.QUAL + # if it's >= maxBestQuality then it's good + else: + return Overview.GOOD + +def dirty_setter(attr_name): + def wrapper(self, val): + if getattr(self, attr_name) != val: + setattr(self, attr_name, val) + self.dirty = True + return wrapper + +class TVEpisode(object): + + def __init__(self, show, season, episode, file=""): + + self._name = "" + self._season = season + self._episode = episode + self._description = "" + self._airdate = datetime.date.fromordinal(1) + self._hasnfo = False + self._hastbn = False + self._status = UNKNOWN + self._tvdbid = 0 + self._file_size = 0 + self._audio_langs = '' + self._release_name = '' + + # setting any of the above sets the dirty flag + self.dirty = True + + self.show = show + self._location = file + + self.lock = threading.Lock() + + self.specifyEpisode(self.season, self.episode) + + self.relatedEps = [] + + self.checkForMetaFiles() + + name = property(lambda self: self._name, dirty_setter("_name")) + season = property(lambda self: self._season, dirty_setter("_season")) + episode = property(lambda self: self._episode, dirty_setter("_episode")) + description = property(lambda self: self._description, dirty_setter("_description")) + airdate = property(lambda self: self._airdate, dirty_setter("_airdate")) + hasnfo = property(lambda self: self._hasnfo, dirty_setter("_hasnfo")) + hastbn = property(lambda self: self._hastbn, dirty_setter("_hastbn")) + status = property(lambda self: self._status, dirty_setter("_status")) + tvdbid = property(lambda self: self._tvdbid, dirty_setter("_tvdbid")) + #location = property(lambda self: self._location, dirty_setter("_location")) + file_size = property(lambda self: self._file_size, dirty_setter("_file_size")) + audio_langs = property(lambda self: self._audio_langs, dirty_setter("_audio_langs")) + release_name = property(lambda self: self._release_name, dirty_setter("_release_name")) + + def _set_location(self, new_location): + logger.log(u"Setter sets location to " + new_location, logger.DEBUG) + + #self._location = newLocation + dirty_setter("_location")(self, new_location) + + if new_location and ek.ek(os.path.isfile, new_location): + self.file_size = ek.ek(os.path.getsize, new_location) + else: + self.file_size = 0 + + location = property(lambda self: self._location, _set_location) + + def checkForMetaFiles(self): + + oldhasnfo = self.hasnfo + oldhastbn = self.hastbn + + cur_nfo = False + cur_tbn = False + + # check for nfo and tbn + if ek.ek(os.path.isfile, self.location): + for cur_provider in sickbeard.metadata_provider_dict.values(): + if cur_provider.episode_metadata: + new_result = cur_provider._has_episode_metadata(self) + else: + new_result = False + cur_nfo = new_result or cur_nfo + + if cur_provider.episode_thumbnails: + new_result = cur_provider._has_episode_thumb(self) + else: + new_result = False + cur_tbn = new_result or cur_tbn + + self.hasnfo = cur_nfo + self.hastbn = cur_tbn + + # if either setting has changed return true, if not return false + return oldhasnfo != self.hasnfo or oldhastbn != self.hastbn + + def specifyEpisode(self, season, episode): + + sqlResult = self.loadFromDB(season, episode) + + if not sqlResult: + # only load from NFO if we didn't load from DB + if ek.ek(os.path.isfile, self.location): + try: + self.loadFromNFO(self.location) + except exceptions.NoNFOException: + logger.log(str(self.show.tvdbid) + ": There was an error loading the NFO for episode " + str(season) + "x" + str(episode), logger.ERROR) + pass + + # if we tried loading it from NFO and didn't find the NFO, use TVDB + if self.hasnfo == False: + try: + result = self.loadFromTVDB(season, episode) + except exceptions.EpisodeDeletedException: + result = False + + # if we failed SQL *and* NFO, TVDB then fail + if result == False: + raise exceptions.EpisodeNotFoundException("Couldn't find episode " + str(season) + "x" + str(episode)) + + # don't update if not needed + if self.dirty: + self.saveToDB() + + def loadFromDB(self, season, episode): + + logger.log(str(self.show.tvdbid) + ": Loading episode details from DB for episode " + str(season) + "x" + str(episode), logger.DEBUG) + + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", [self.show.tvdbid, season, episode]) + + if len(sqlResults) > 1: + raise exceptions.MultipleDBEpisodesException("Your DB has two records for the same show somehow.") + elif len(sqlResults) == 0: + logger.log(str(self.show.tvdbid) + ": Episode " + str(self.season) + "x" + str(self.episode) + " not found in the database", logger.DEBUG) + return False + else: + #NAMEIT logger.log(u"AAAAA from" + str(self.season)+"x"+str(self.episode) + " -" + self.name + " to " + str(sqlResults[0]["name"])) + if sqlResults[0]["name"] != None: + self.name = sqlResults[0]["name"] + self.season = season + self.episode = episode + self.description = sqlResults[0]["description"] + if self.description == None: + self.description = "" + self.airdate = datetime.date.fromordinal(int(sqlResults[0]["airdate"])) + #logger.log(u"1 Status changes from " + str(self.status) + " to " + str(sqlResults[0]["status"]), logger.DEBUG) + self.status = int(sqlResults[0]["status"]) + + # don't overwrite my location + if sqlResults[0]["location"] != "" and sqlResults[0]["location"] != None: + self.location = os.path.normpath(sqlResults[0]["location"]) + if sqlResults[0]["file_size"]: + self.file_size = int(sqlResults[0]["file_size"]) + else: + self.file_size = 0 + + self.tvdbid = int(sqlResults[0]["tvdbid"]) + + if sqlResults[0]["audio_langs"] != None: + self.audio_langs = str(sqlResults[0]["audio_langs"]).split("|") + + if sqlResults[0]["release_name"] != None: + self.release_name = sqlResults[0]["release_name"] + + self.dirty = False + return True + + def loadFromTVDB(self, season=None, episode=None, cache=True, tvapi=None, cachedSeason=None): + + if season == None: + season = self.season + if episode == None: + episode = self.episode + + logger.log(str(self.show.tvdbid) + ": Loading episode details from theTVDB for episode " + str(season) + "x" + str(episode), logger.DEBUG) + + tvdb_lang = self.show.lang + + try: + if cachedSeason is None: + if tvapi is None: + # There's gotta be a better way of doing this but we don't wanna + # change the cache value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if not cache: + ltvdb_api_parms['cache'] = False + + if tvdb_lang: + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + else: + t = tvapi + myEp = t[self.show.tvdbid][season][episode] + else: + myEp = cachedSeason[episode] + + except (tvdb_exceptions.tvdb_error, IOError), e: + logger.log(u"TVDB threw up an error: "+ex(e), logger.DEBUG) + # if the episode is already valid just log it, if not throw it up + if self.name: + logger.log(u"TVDB timed out but we have enough info from other sources, allowing the error", logger.DEBUG) + return + else: + logger.log(u"TVDB timed out, unable to create the episode", logger.ERROR) + return False + except (tvdb_exceptions.tvdb_episodenotfound, tvdb_exceptions.tvdb_seasonnotfound): + logger.log(u"Unable to find the episode on tvdb... has it been removed? Should I delete from db?", logger.DEBUG) + # if I'm no longer on TVDB but I once was then delete myself from the DB + if self.tvdbid != -1: + self.deleteEpisode() + return + + + if not myEp["firstaired"] or myEp["firstaired"] == "0000-00-00": + myEp["firstaired"] = str(datetime.date.fromordinal(1)) + + if myEp["episodename"] == None or myEp["episodename"] == "": + logger.log(u"This episode ("+self.show.name+" - "+str(season)+"x"+str(episode)+") has no name on TVDB") + # if I'm incomplete on TVDB but I once was complete then just delete myself from the DB for now + if self.tvdbid != -1: + self.deleteEpisode() + return False + + #NAMEIT logger.log(u"BBBBBBBB from " + str(self.season)+"x"+str(self.episode) + " -" +self.name+" to "+myEp["episodename"]) + self.name = myEp["episodename"] + self.season = season + self.episode = episode + tmp_description = myEp["overview"] + if tmp_description == None: + self.description = "" + else: + self.description = tmp_description + rawAirdate = [int(x) for x in myEp["firstaired"].split("-")] + try: + self.airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2]) + except ValueError: + logger.log(u"Malformed air date retrieved from TVDB ("+self.show.name+" - "+str(season)+"x"+str(episode)+")", logger.ERROR) + # if I'm incomplete on TVDB but I once was complete then just delete myself from the DB for now + if self.tvdbid != -1: + self.deleteEpisode() + return False + + #early conversion to int so that episode doesn't get marked dirty + self.tvdbid = int(myEp["id"]) + + #don't update show status if show dir is missing, unless missing show dirs are created during post-processing + if not ek.ek(os.path.isdir, self.show._location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: + logger.log(u"The show dir is missing, not bothering to change the episode statuses since it'd probably be invalid") + return + + logger.log(str(self.show.tvdbid) + ": Setting status for " + str(season) + "x" + str(episode) + " based on status " + str(self.status) + " and existence of " + self.location, logger.DEBUG) + + if not ek.ek(os.path.isfile, self.location): + + # if we don't have the file + if self.airdate >= datetime.date.today() and self.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER: + # and it hasn't aired yet set the status to UNAIRED + logger.log(u"Episode airs in the future, changing status from " + str(self.status) + " to " + str(UNAIRED), logger.DEBUG) + self.status = UNAIRED + # if there's no airdate then set it to skipped (and respect ignored) + elif self.airdate == datetime.date.fromordinal(1): + if self.status == IGNORED: + logger.log(u"Episode has no air date, but it's already marked as ignored", logger.DEBUG) + else: + logger.log(u"Episode has no air date, automatically marking it skipped", logger.DEBUG) + self.status = SKIPPED + # if we don't have the file and the airdate is in the past + else: + if self.status == UNAIRED: + self.status = WANTED + + # if we somehow are still UNKNOWN then just skip it + elif self.status == UNKNOWN: + self.status = SKIPPED + + else: + logger.log(u"Not touching status because we have no ep file, the airdate is in the past, and the status is "+str(self.status), logger.DEBUG) + + # if we have a media file then it's downloaded + elif sickbeard.helpers.isMediaFile(self.location): + # leave propers alone, you have to either post-process them or manually change them back + if self.status not in Quality.SNATCHED_PROPER + Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED]: + logger.log(u"5 Status changes from " + str(self.status) + " to " + str(Quality.statusFromName(self.location)), logger.DEBUG) + self.status = Quality.statusFromName(self.location) + + # shouldn't get here probably + else: + logger.log(u"6 Status changes from " + str(self.status) + " to " + str(UNKNOWN), logger.DEBUG) + self.status = UNKNOWN + + + # hasnfo, hastbn, status? + + + def loadFromNFO(self, location): + + if not os.path.isdir(self.show._location): + logger.log(str(self.show.tvdbid) + ": The show dir is missing, not bothering to try loading the episode NFO") + return + + logger.log(str(self.show.tvdbid) + ": Loading episode details from the NFO file associated with " + location, logger.DEBUG) + + self.location = location + + if self.location != "": + + if self.status == UNKNOWN: + if sickbeard.helpers.isMediaFile(self.location): + logger.log(u"7 Status changes from " + str(self.status) + " to " + str(Quality.statusFromName(self.location)), logger.DEBUG) + self.status = Quality.statusFromName(self.location) + + nfoFile = sickbeard.helpers.replaceExtension(self.location, "nfo") + logger.log(str(self.show.tvdbid) + ": Using NFO name " + nfoFile, logger.DEBUG) + + if ek.ek(os.path.isfile, nfoFile): + try: + showXML = etree.ElementTree(file = nfoFile) + except (SyntaxError, ValueError), e: + logger.log(u"Error loading the NFO, backing up the NFO and skipping for now: " + ex(e), logger.ERROR) #TODO: figure out what's wrong and fix it + try: + ek.ek(os.rename, nfoFile, nfoFile + ".old") + except Exception, e: + logger.log(u"Failed to rename your episode's NFO file - you need to delete it or fix it: " + ex(e), logger.ERROR) + raise exceptions.NoNFOException("Error in NFO format") + + for epDetails in showXML.getiterator('episodedetails'): + if epDetails.findtext('season') == None or int(epDetails.findtext('season')) != self.season or \ + epDetails.findtext('episode') == None or int(epDetails.findtext('episode')) != self.episode: + logger.log(str(self.show.tvdbid) + ": NFO has an <episodedetails> block for a different episode - wanted " + str(self.season) + "x" + str(self.episode) + " but got " + str(epDetails.findtext('season')) + "x" + str(epDetails.findtext('episode')), logger.DEBUG) + continue + + if epDetails.findtext('title') == None or epDetails.findtext('aired') == None: + raise exceptions.NoNFOException("Error in NFO format (missing episode title or airdate)") + + self.name = epDetails.findtext('title') + self.episode = int(epDetails.findtext('episode')) + self.season = int(epDetails.findtext('season')) + + self.description = epDetails.findtext('plot') + if self.description == None: + self.description = "" + + if epDetails.findtext('aired'): + rawAirdate = [int(x) for x in epDetails.findtext('aired').split("-")] + self.airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2]) + else: + self.airdate = datetime.date.fromordinal(1) + + self.hasnfo = True + else: + self.hasnfo = False + + if ek.ek(os.path.isfile, sickbeard.helpers.replaceExtension(nfoFile, "tbn")): + self.hastbn = True + else: + self.hastbn = False + + def __str__ (self): + + toReturn = "" + toReturn += str(self.show.name) + " - " + str(self.season) + "x" + str(self.episode) + " - " + str(self.name) + "\n" + toReturn += "location: " + str(self.location) + "\n" + toReturn += "description: " + str(self.description) + "\n" + toReturn += "airdate: " + str(self.airdate.toordinal()) + " (" + str(self.airdate) + ")\n" + toReturn += "hasnfo: " + str(self.hasnfo) + "\n" + toReturn += "hastbn: " + str(self.hastbn) + "\n" + toReturn += "status: " + str(self.status) + "\n" + toReturn += "languages: " + ",".join(self.audio_langs) + "\n" + return toReturn + + def createMetaFiles(self, force=False): + + if not ek.ek(os.path.isdir, self.show._location): + logger.log(str(self.show.tvdbid) + ": The show dir is missing, not bothering to try to create metadata") + return + + self.createNFO(force) + self.createThumbnail(force) + + if self.checkForMetaFiles(): + self.saveToDB() + + def createNFO(self, force=False): + + result = False + + for cur_provider in sickbeard.metadata_provider_dict.values(): + result = cur_provider.create_episode_metadata(self) or result + + return result + + def createThumbnail(self, force=False): + + result = False + + for cur_provider in sickbeard.metadata_provider_dict.values(): + result = cur_provider.create_episode_thumb(self) or result + + return result + + def deleteEpisode(self): + + logger.log(u"Deleting "+self.show.name+" "+str(self.season)+"x"+str(self.episode)+" from the DB", logger.DEBUG) + + # remove myself from the show dictionary + if self.show.getEpisode(self.season, self.episode, noCreate=True) == self: + logger.log(u"Removing myself from my show's list", logger.DEBUG) + del self.show.episodes[self.season][self.episode] + + # delete myself from the DB + logger.log(u"Deleting myself from the database", logger.DEBUG) + myDB = db.DBConnection() + sql = "DELETE FROM tv_episodes WHERE showid="+str(self.show.tvdbid)+" AND season="+str(self.season)+" AND episode="+str(self.episode) + myDB.action(sql) + + raise exceptions.EpisodeDeletedException() + + def saveToDB(self, forceSave=False): + """ + Saves this episode to the database if any of its data has been changed since the last save. + + forceSave: If True it will save to the database even if no data has been changed since the + last save (aka if the record is not dirty). + """ + + if not self.dirty and not forceSave: + logger.log(str(self.show.tvdbid) + ": Not saving episode to db - record is not dirty", logger.DEBUG) + return + + logger.log(str(self.show.tvdbid) + ": Saving episode details to database", logger.DEBUG) + + logger.log(u"STATUS IS " + str(self.status), logger.DEBUG) + + myDB = db.DBConnection() + newValueDict = {"tvdbid": self.tvdbid, + "name": self.name, + "description": self.description, + "airdate": self.airdate.toordinal(), + "hasnfo": self.hasnfo, + "hastbn": self.hastbn, + "status": self.status, + "location": self.location, + "audio_langs": "|".join(self.audio_langs), + "file_size": self.file_size, + "release_name": self.release_name} + controlValueDict = {"showid": self.show.tvdbid, + "season": self.season, + "episode": self.episode} + + # use a custom update/insert method to get the data into the DB + myDB.upsert("tv_episodes", newValueDict, controlValueDict) + + def fullPath (self): + if self.location == None or self.location == "": + return None + else: + return ek.ek(os.path.join, self.show.location, self.location) + + def prettyName(self): + """ + Returns the name of this episode in a "pretty" human-readable format. Used for logging + and notifications and such. + + Returns: A string representing the episode's name and season/ep numbers + """ + + return self._format_pattern('%SN - %Sx%0E - %EN') + + def _ep_name(self): + """ + Returns the name of the episode to use during renaming. Combines the names of related episodes. + Eg. "Ep Name (1)" and "Ep Name (2)" becomes "Ep Name" + "Ep Name" and "Other Ep Name" becomes "Ep Name & Other Ep Name" + """ + + multiNameRegex = "(.*) \(\d\)" + + self.relatedEps = sorted(self.relatedEps, key=lambda x: x.episode) + + if len(self.relatedEps) == 0: + goodName = self.name + + else: + goodName = '' + + singleName = True + curGoodName = None + + for curName in [self.name] + [x.name for x in self.relatedEps]: + match = re.match(multiNameRegex, curName) + if not match: + singleName = False + break + + if curGoodName == None: + curGoodName = match.group(1) + elif curGoodName != match.group(1): + singleName = False + break + + if singleName: + goodName = curGoodName + else: + goodName = self.name + for relEp in self.relatedEps: + goodName += " & " + relEp.name + + return goodName + + def _replace_map(self): + """ + Generates a replacement map for this episode which maps all possible custom naming patterns to the correct + value for this episode. + + Returns: A dict with patterns as the keys and their replacement values as the values. + """ + + ep_name = self._ep_name() + + def dot(name): + return helpers.sanitizeSceneName(name) + + def us(name): + return re.sub('[ -]','_', name) + + def release_name(name): + if name and name.lower().endswith('.nzb'): + name = name.rpartition('.')[0] + return name + + def release_group(name): + if not name: + return '' + + np = NameParser(name) + + try: + parse_result = np.parse(name) + except InvalidNameException, e: + logger.log(u"Unable to get parse release_group: "+ex(e), logger.DEBUG) + return '' + + if not parse_result.release_group: + return '' + return parse_result.release_group + + epStatus, epQual = Quality.splitCompositeStatus(self.status) #@UnusedVariable + + return { + '%SN': self.show.name, + '%S.N': dot(self.show.name), + '%S_N': us(self.show.name), + '%EN': ep_name, + '%E.N': dot(ep_name), + '%E_N': us(ep_name), + '%QN': Quality.qualityStrings[epQual], + '%Q.N': dot(Quality.qualityStrings[epQual]), + '%Q_N': us(Quality.qualityStrings[epQual]), + '%S': str(self.season), + '%0S': '%02d' % self.season, + '%E': str(self.episode), + '%0E': '%02d' % self.episode, + '%RN': release_name(self.release_name), + '%RG': release_group(self.release_name), + '%AD': str(self.airdate).replace('-', ' '), + '%A.D': str(self.airdate).replace('-', '.'), + '%A_D': us(str(self.airdate)), + '%A-D': str(self.airdate), + '%Y': str(self.airdate.year), + '%M': str(self.airdate.month), + '%D': str(self.airdate.day), + '%0M': '%02d' % self.airdate.month, + '%0D': '%02d' % self.airdate.day, + } + + def _format_string(self, pattern, replace_map): + """ + Replaces all template strings with the correct value + """ + + result_name = pattern + + # do the replacements + for cur_replacement in sorted(replace_map.keys(), reverse=True): + result_name = result_name.replace(cur_replacement, helpers.sanitizeFileName(replace_map[cur_replacement])) + result_name = result_name.replace(cur_replacement.lower(), helpers.sanitizeFileName(replace_map[cur_replacement].lower())) + + return result_name + + def _format_pattern(self, pattern=None, multi=None): + """ + Manipulates an episode naming pattern and then fills the template in + """ + + if pattern == None: + pattern = sickbeard.NAMING_PATTERN + + if multi == None: + multi = sickbeard.NAMING_MULTI_EP + + replace_map = self._replace_map() + + result_name = pattern + + # if there's no release group then replace it with a reasonable facsimile + if not replace_map['%RN']: + if self.show.air_by_date: + result_name = result_name.replace('%RN', '%S.N.%A.D.%E.N-SiCKBEARD') + result_name = result_name.replace('%rn', '%s.n.%A.D.%e.n-sickbeard') + else: + result_name = result_name.replace('%RN', '%S.N.S%0SE%0E.%E.N-SiCKBEARD') + result_name = result_name.replace('%rn', '%s.n.s%0se%0e.%e.n-sickbeard') + + result_name = result_name.replace('%RG', 'SiCKBEARD') + result_name = result_name.replace('%rg', 'sickbeard') + logger.log(u"Episode has no release name, replacing it with a generic one: "+result_name, logger.DEBUG) + + # split off ep name part only + name_groups = re.split(r'[\\/]', result_name) + + # figure out the double-ep numbering style for each group, if applicable + for cur_name_group in name_groups: + + season_format = sep = ep_sep = ep_format = None + + season_ep_regex = ''' + (?P<pre_sep>[ _.-]*) + ((?:s(?:eason|eries)?\s*)?%0?S(?![._]?N)) + (.*?) + (%0?E(?![._]?N)) + (?P<post_sep>[ _.-]*) + ''' + ep_only_regex = '(E?%0?E(?![._]?N))' + + # try the normal way + season_ep_match = re.search(season_ep_regex, cur_name_group, re.I|re.X) + ep_only_match = re.search(ep_only_regex, cur_name_group, re.I|re.X) + + # if we have a season and episode then collect the necessary data + if season_ep_match: + season_format = season_ep_match.group(2) + ep_sep = season_ep_match.group(3) + ep_format = season_ep_match.group(4) + sep = season_ep_match.group('pre_sep') + if not sep: + sep = season_ep_match.group('post_sep') + if not sep: + sep = ' ' + + # force 2-3-4 format if they chose to extend + if multi in (NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_LIMITED_EXTEND_E_PREFIXED): + ep_sep = '-' + + regex_used = season_ep_regex + + # if there's no season then there's not much choice so we'll just force them to use 03-04-05 style + elif ep_only_match: + season_format = '' + ep_sep = '-' + ep_format = ep_only_match.group(1) + sep = '' + regex_used = ep_only_regex + + else: + continue + + # we need at least this much info to continue + if not ep_sep or not ep_format: + continue + + # start with the ep string, eg. E03 + ep_string = self._format_string(ep_format.upper(), replace_map) + for other_ep in self.relatedEps: + + # for limited extend we only append the last ep + if multi in (NAMING_LIMITED_EXTEND, NAMING_LIMITED_EXTEND_E_PREFIXED) and other_ep != self.relatedEps[-1]: + continue + + elif multi == NAMING_DUPLICATE: + # add " - S01" + ep_string += sep + season_format + + elif multi == NAMING_SEPARATED_REPEAT: + ep_string += sep + + # add "E04" + ep_string += ep_sep + + if multi == NAMING_LIMITED_EXTEND_E_PREFIXED: + ep_string += 'E' + + ep_string += other_ep._format_string(ep_format.upper(), other_ep._replace_map()) + + if season_ep_match: + regex_replacement = r'\g<pre_sep>\g<2>\g<3>' + ep_string + r'\g<post_sep>' + elif ep_only_match: + regex_replacement = ep_string + + # fill out the template for this piece and then insert this piece into the actual pattern + cur_name_group_result = re.sub('(?i)(?x)'+regex_used, regex_replacement, cur_name_group) + #cur_name_group_result = cur_name_group.replace(ep_format, ep_string) + #logger.log(u"found "+ep_format+" as the ep pattern using "+regex_used+" and replaced it with "+regex_replacement+" to result in "+cur_name_group_result+" from "+cur_name_group, logger.DEBUG) + result_name = result_name.replace(cur_name_group, cur_name_group_result) + + result_name = self._format_string(result_name, replace_map) + + logger.log(u"formatting pattern: "+pattern+" -> "+result_name, logger.DEBUG) + + + return result_name + + def proper_path(self): + """ + Figures out the path where this episode SHOULD live according to the renaming rules, relative from the show dir + """ + + result = self.formatted_filename() + + # if they want us to flatten it and we're allowed to flatten it then we will + if self.show.flatten_folders and not sickbeard.NAMING_FORCE_FOLDERS: + return result + + # if not we append the folder on and use that + else: + result = ek.ek(os.path.join, self.formatted_dir(), result) + + return result + + + def formatted_dir(self, pattern=None, multi=None): + """ + Just the folder name of the episode + """ + + if pattern == None: + # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep + if self.show.air_by_date and sickbeard.NAMING_CUSTOM_ABD and not self.relatedEps: + pattern = sickbeard.NAMING_ABD_PATTERN + else: + pattern = sickbeard.NAMING_PATTERN + + # split off the dirs only, if they exist + name_groups = re.split(r'[\\/]', pattern) + + if len(name_groups) == 1: + return '' + else: + return self._format_pattern(os.sep.join(name_groups[:-1]), multi) + + + def formatted_filename(self, pattern=None, multi=None): + """ + Just the filename of the episode, formatted based on the naming settings + """ + + if pattern == None: + # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep + if self.show.air_by_date and sickbeard.NAMING_CUSTOM_ABD and not self.relatedEps: + pattern = sickbeard.NAMING_ABD_PATTERN + else: + pattern = sickbeard.NAMING_PATTERN + + # split off the filename only, if they exist + name_groups = re.split(r'[\\/]', pattern) + + return self._format_pattern(name_groups[-1], multi) + + def rename(self): + """ + Renames an episode file and all related files to the location and filename as specified + in the naming settings. + """ + + if not ek.ek(os.path.isfile, self.location): + logger.log(u"Can't perform rename on " + self.location + " when it doesn't exist, skipping", logger.WARNING) + return + + proper_path = self.proper_path() + absolute_proper_path = ek.ek(os.path.join, self.show.location, proper_path) + absolute_current_path_no_ext, file_ext = os.path.splitext(self.location) + + current_path = absolute_current_path_no_ext + + if absolute_current_path_no_ext.startswith(self.show.location): + current_path = absolute_current_path_no_ext[len(self.show.location):] + + logger.log(u"Renaming/moving episode from the base path " + self.location + " to " + absolute_proper_path, logger.DEBUG) + + # if it's already named correctly then don't do anything + if proper_path == current_path: + logger.log(str(self.tvdbid) + ": File " + self.location + " is already named correctly, skipping", logger.DEBUG) + return + + related_files = postProcessor.PostProcessor(self.location)._list_associated_files(self.location) + logger.log(u"Files associated to " + self.location + ": " + str(related_files), logger.DEBUG) + + # move the ep file + result = helpers.rename_ep_file(self.location, absolute_proper_path) + + # move related files + for cur_related_file in related_files: + cur_result = helpers.rename_ep_file(cur_related_file, absolute_proper_path) + if cur_result == False: + logger.log(str(self.tvdbid) + ": Unable to rename file " + cur_related_file, logger.ERROR) + + # save the ep + with self.lock: + if result != False: + self.location = absolute_proper_path + file_ext + for relEp in self.relatedEps: + relEp.location = absolute_proper_path + file_ext + + # in case something changed with the metadata just do a quick check + for curEp in [self] + self.relatedEps: + curEp.checkForMetaFiles() + + # save any changes to the database + with self.lock: + self.saveToDB() + for relEp in self.relatedEps: + relEp.saveToDB() diff --git a/sickbeard/tvcache.py b/sickbeard/tvcache.py index 521864a78dd7423d5991de2d1c2d51444c6497fd..5c9426cf99f2c25c87f46222181f157f39ece066 100644 --- a/sickbeard/tvcache.py +++ b/sickbeard/tvcache.py @@ -1,395 +1,405 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import time -import datetime -import sqlite3 - -import sickbeard - -from sickbeard import db -from sickbeard import logger -from sickbeard.common import Quality - -from sickbeard import helpers, exceptions, show_name_helpers -from sickbeard import name_cache -from sickbeard.exceptions import ex - -#import xml.etree.cElementTree as etree -import xml.dom.minidom - -from lib.tvdb_api import tvdb_api, tvdb_exceptions - -from name_parser.parser import NameParser, InvalidNameException - - -class CacheDBConnection(db.DBConnection): - - def __init__(self, providerName): - db.DBConnection.__init__(self, "cache.db") - - # Create the table if it's not already there - try: - sql = "CREATE TABLE "+providerName+" (name TEXT, season NUMERIC, episodes TEXT, tvrid NUMERIC, tvdbid NUMERIC, url TEXT, time NUMERIC, quality TEXT);" - self.connection.execute(sql) - self.connection.commit() - except sqlite3.OperationalError, e: - if str(e) != "table "+providerName+" already exists": - raise - - # Create the table if it's not already there - try: - sql = "CREATE TABLE lastUpdate (provider TEXT, time NUMERIC);" - self.connection.execute(sql) - self.connection.commit() - except sqlite3.OperationalError, e: - if str(e) != "table lastUpdate already exists": - raise - -class TVCache(): - - def __init__(self, provider): - - self.provider = provider - self.providerID = self.provider.getID() - self.minTime = 10 - - def _getDB(self): - - return CacheDBConnection(self.providerID) - - def _clearCache(self): - - myDB = self._getDB() - - myDB.action("DELETE FROM "+self.providerID+" WHERE 1") - - def _getRSSData(self): - - data = None - - return data - - def _checkAuth(self, data): - return True - - def _checkItemAuth(self, title, url): - return True - - def updateCache(self): - - if not self.shouldUpdate(): - return - - data = self._getRSSData() - - # as long as the http request worked we count this as an update - if data: - self.setLastUpdate() - else: - return [] - - # now that we've loaded the current RSS feed lets delete the old cache - logger.log(u"Clearing "+self.provider.name+" cache and updating with new information") - self._clearCache() - - if not self._checkAuth(data): - raise exceptions.AuthException("Your authentication info for "+self.provider.name+" is incorrect, check your config") - - try: - parsedXML = xml.dom.minidom.parseString(data) - items = parsedXML.getElementsByTagName('item') - except Exception, e: - logger.log(u"Error trying to load "+self.provider.name+" RSS feed: "+ex(e), logger.ERROR) - logger.log(u"Feed contents: "+repr(data), logger.DEBUG) - return [] - - if parsedXML.documentElement.tagName != 'rss': - logger.log(u"Resulting XML from "+self.provider.name+" isn't RSS, not parsing it", logger.ERROR) - return [] - - for item in items: - - self._parseItem(item) - - def _translateLinkURL(self, url): - return url.replace('&','&') - - def _parseItem(self, item): - - title = helpers.get_xml_text(item.getElementsByTagName('title')[0]) - url = helpers.get_xml_text(item.getElementsByTagName('link')[0]) - - self._checkItemAuth(title, url) - - if not title or not url: - logger.log(u"The XML returned from the "+self.provider.name+" feed is incomplete, this result is unusable", logger.ERROR) - return - - url = self._translateLinkURL(url) - - logger.log(u"Adding item from RSS to cache: "+title, logger.DEBUG) - - self._addCacheEntry(title, url) - - def _getLastUpdate(self): - myDB = self._getDB() - sqlResults = myDB.select("SELECT time FROM lastUpdate WHERE provider = ?", [self.providerID]) - - if sqlResults: - lastTime = int(sqlResults[0]["time"]) - else: - lastTime = 0 - - return datetime.datetime.fromtimestamp(lastTime) - - def setLastUpdate(self, toDate=None): - - if not toDate: - toDate = datetime.datetime.today() - - myDB = self._getDB() - myDB.upsert("lastUpdate", - {'time': int(time.mktime(toDate.timetuple()))}, - {'provider': self.providerID}) - - lastUpdate = property(_getLastUpdate) - - def shouldUpdate(self): - # if we've updated recently then skip the update - if datetime.datetime.today() - self.lastUpdate < datetime.timedelta(minutes=self.minTime): - logger.log(u"Last update was too soon, using old cache: today()-"+str(self.lastUpdate)+"<"+str(datetime.timedelta(minutes=self.minTime)), logger.DEBUG) - return False - - return True - - def _addCacheEntry(self, name, url, season=None, episodes=None, tvdb_id=0, tvrage_id=0, quality=None, extraNames=[]): - - myDB = self._getDB() - - parse_result = None - - # if we don't have complete info then parse the filename to get it - for curName in [name] + extraNames: - try: - myParser = NameParser() - parse_result = myParser.parse(curName) - except InvalidNameException: - logger.log(u"Unable to parse the filename "+curName+" into a valid episode", logger.DEBUG) - continue - - if not parse_result: - logger.log(u"Giving up because I'm unable to parse this name: "+name, logger.DEBUG) - return False - - if not parse_result.series_name: - logger.log(u"No series name retrieved from "+name+", unable to cache it", logger.DEBUG) - return False - - tvdb_lang = None - - # if we need tvdb_id or tvrage_id then search the DB for them - if not tvdb_id or not tvrage_id: - - # if we have only the tvdb_id, use the database - if tvdb_id: - showObj = helpers.findCertainShow(sickbeard.showList, tvdb_id) - if showObj: - tvrage_id = showObj.tvrid - tvdb_lang = showObj.lang - else: - logger.log(u"We were given a TVDB id "+str(tvdb_id)+" but it doesn't match a show we have in our list, so leaving tvrage_id empty", logger.DEBUG) - tvrage_id = 0 - - # if we have only a tvrage_id then use the database - elif tvrage_id: - showObj = helpers.findCertainTVRageShow(sickbeard.showList, tvrage_id) - if showObj: - tvdb_id = showObj.tvdbid - tvdb_lang = showObj.lang - else: - logger.log(u"We were given a TVRage id "+str(tvrage_id)+" but it doesn't match a show we have in our list, so leaving tvdb_id empty", logger.DEBUG) - tvdb_id = 0 - - # if they're both empty then fill out as much info as possible by searching the show name - else: - - # check the name cache and see if we already know what show this is - logger.log(u"Checking the cache to see if we already know the tvdb id of "+parse_result.series_name, logger.DEBUG) - tvdb_id = name_cache.retrieveNameFromCache(parse_result.series_name) - - # remember if the cache lookup worked or not so we know whether we should bother updating it later - if tvdb_id == None: - logger.log(u"No cache results returned, continuing on with the search", logger.DEBUG) - from_cache = False - else: - logger.log(u"Cache lookup found "+repr(tvdb_id)+", using that", logger.DEBUG) - from_cache = True - - # if the cache failed, try looking up the show name in the database - if tvdb_id == None: - logger.log(u"Trying to look the show up in the show database", logger.DEBUG) - showResult = helpers.searchDBForShow(parse_result.series_name) - if showResult: - logger.log(parse_result.series_name+" was found to be show "+showResult[1]+" ("+str(showResult[0])+") in our DB.", logger.DEBUG) - tvdb_id = showResult[0] - - # if the DB lookup fails then do a comprehensive regex search - if tvdb_id == None: - logger.log(u"Couldn't figure out a show name straight from the DB, trying a regex search instead", logger.DEBUG) - for curShow in sickbeard.showList: - if show_name_helpers.isGoodResult(name, curShow, False): - logger.log(u"Successfully matched "+name+" to "+curShow.name+" with regex", logger.DEBUG) - tvdb_id = curShow.tvdbid - tvdb_lang = curShow.lang - break - - # if tvdb_id was anything but None (0 or a number) then - if not from_cache: - name_cache.addNameToCache(parse_result.series_name, tvdb_id) - - # if we came out with tvdb_id = None it means we couldn't figure it out at all, just use 0 for that - if tvdb_id == None: - tvdb_id = 0 - - # if we found the show then retrieve the show object - if tvdb_id: - showObj = helpers.findCertainShow(sickbeard.showList, tvdb_id) - if showObj: - tvrage_id = showObj.tvrid - tvdb_lang = showObj.lang - - # if we weren't provided with season/episode information then get it from the name that we parsed - if not season: - season = parse_result.season_number if parse_result.season_number != None else 1 - if not episodes: - episodes = parse_result.episode_numbers - - # if we have an air-by-date show then get the real season/episode numbers - if parse_result.air_by_date and tvdb_id: - try: - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if not (tvdb_lang == "" or tvdb_lang == "en" or tvdb_lang == None): - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - epObj = t[tvdb_id].airedOn(parse_result.air_date)[0] - season = int(epObj["seasonnumber"]) - episodes = [int(epObj["episodenumber"])] - except tvdb_exceptions.tvdb_episodenotfound: - logger.log(u"Unable to find episode with date "+str(parse_result.air_date)+" for show "+parse_result.series_name+", skipping", logger.WARNING) - return False - except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to contact TVDB: "+ex(e), logger.WARNING) - return False - - episodeText = "|"+"|".join(map(str, episodes))+"|" - - # get the current timestamp - curTimestamp = int(time.mktime(datetime.datetime.today().timetuple())) - - if not quality: - quality = Quality.nameQuality(name) - - myDB.action("INSERT INTO "+self.providerID+" (name, season, episodes, tvrid, tvdbid, url, time, quality) VALUES (?,?,?,?,?,?,?,?)", - [name, season, episodeText, tvrage_id, tvdb_id, url, curTimestamp, quality]) - - - def searchCache(self, episode, manualSearch=False): - neededEps = self.findNeededEpisodes(episode, manualSearch) - return neededEps[episode] - - def listPropers(self, date=None, delimiter="."): - - myDB = self._getDB() - - sql = "SELECT * FROM "+self.providerID+" WHERE name LIKE '%.PROPER.%' OR name LIKE '%.REPACK.%'" - - if date != None: - sql += " AND time >= "+str(int(time.mktime(date.timetuple()))) - - #return filter(lambda x: x['tvdbid'] != 0, myDB.select(sql)) - return myDB.select(sql) - - def findNeededEpisodes(self, episode = None, manualSearch=False): - neededEps = {} - - if episode: - neededEps[episode] = [] - - myDB = self._getDB() - - if not episode: - sqlResults = myDB.select("SELECT * FROM "+self.providerID) - else: - sqlResults = myDB.select("SELECT * FROM "+self.providerID+" WHERE tvdbid = ? AND season = ? AND episodes LIKE ?", [episode.show.tvdbid, episode.season, "%|"+str(episode.episode)+"|%"]) - - # for each cache entry - for curResult in sqlResults: - - # skip non-tv crap (but allow them for Newzbin cause we assume it's filtered well) - if self.providerID != 'newzbin' and not show_name_helpers.filterBadReleases(curResult["name"]): - continue - - # get the show object, or if it's not one of our shows then ignore it - showObj = helpers.findCertainShow(sickbeard.showList, int(curResult["tvdbid"])) - if not showObj: - continue - - # get season and ep data (ignoring multi-eps for now) - curSeason = int(curResult["season"]) - if curSeason == -1: - continue - curEp = curResult["episodes"].split("|")[1] - if not curEp: - continue - curEp = int(curEp) - curQuality = int(curResult["quality"]) - - # if the show says we want that episode then add it to the list - if not showObj.wantEpisode(curSeason, curEp, curQuality, manualSearch): - logger.log(u"Skipping "+curResult["name"]+" because we don't want an episode that's "+Quality.qualityStrings[curQuality], logger.DEBUG) - - else: - - if episode: - epObj = episode - else: - epObj = showObj.getEpisode(curSeason, curEp) - - # build a result object - title = curResult["name"] - url = curResult["url"] - - logger.log(u"Found result " + title + " at " + url) - - result = self.provider.getResult([epObj]) - result.url = url - result.name = title - result.quality = curQuality - - # add it to the list - if epObj not in neededEps: - neededEps[epObj] = [result] - else: - neededEps[epObj].append(result) - - return neededEps +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import time +import datetime +import sqlite3 + +import sickbeard + +from sickbeard import db +from sickbeard import logger +from sickbeard.common import Quality + +from sickbeard import helpers, exceptions, show_name_helpers +from sickbeard import name_cache +from sickbeard.exceptions import ex + +#import xml.etree.cElementTree as etree +import xml.dom.minidom + +from lib.tvdb_api import tvdb_api, tvdb_exceptions + +from name_parser.parser import NameParser, InvalidNameException + + +class CacheDBConnection(db.DBConnection): + + def __init__(self, providerName): + db.DBConnection.__init__(self, "cache.db") + + # Create the table if it's not already there + try: + sql = "CREATE TABLE "+providerName+" (name TEXT, season NUMERIC, episodes TEXT, tvrid NUMERIC, tvdbid NUMERIC, url TEXT, time NUMERIC, quality TEXT);" + self.connection.execute(sql) + self.connection.commit() + except sqlite3.OperationalError, e: + if str(e) != "table "+providerName+" already exists": + raise + + # Create the table if it's not already there + try: + sql = "CREATE TABLE lastUpdate (provider TEXT, time NUMERIC);" + self.connection.execute(sql) + self.connection.commit() + except sqlite3.OperationalError, e: + if str(e) != "table lastUpdate already exists": + raise + +class TVCache(): + + def __init__(self, provider): + + self.provider = provider + self.providerID = self.provider.getID() + self.minTime = 10 + + def _getDB(self): + + return CacheDBConnection(self.providerID) + + def _clearCache(self): + + myDB = self._getDB() + + myDB.action("DELETE FROM "+self.providerID+" WHERE 1") + + def _getRSSData(self): + + data = None + + return data + + def _checkAuth(self, data): + return True + + def _checkItemAuth(self, title, url): + return True + + def updateCache(self): + + if not self.shouldUpdate(): + return + + data = self._getRSSData() + + # as long as the http request worked we count this as an update + if data: + self.setLastUpdate() + else: + return [] + + # now that we've loaded the current RSS feed lets delete the old cache + logger.log(u"Clearing "+self.provider.name+" cache and updating with new information") + self._clearCache() + + if not self._checkAuth(data): + raise exceptions.AuthException("Your authentication info for "+self.provider.name+" is incorrect, check your config") + + try: + parsedXML = xml.dom.minidom.parseString(data) + items = parsedXML.getElementsByTagName('item') + except Exception, e: + logger.log(u"Error trying to load "+self.provider.name+" RSS feed: "+ex(e), logger.ERROR) + logger.log(u"Feed contents: "+repr(data), logger.DEBUG) + return [] + + if parsedXML.documentElement.tagName != 'rss': + logger.log(u"Resulting XML from "+self.provider.name+" isn't RSS, not parsing it", logger.ERROR) + return [] + + for item in items: + + self._parseItem(item) + + def _translateLinkURL(self, url): + return url.replace('&','&') + + def _parseItem(self, item): + + title = helpers.get_xml_text(item.getElementsByTagName('title')[0]) + url = helpers.get_xml_text(item.getElementsByTagName('link')[0]) + + self._checkItemAuth(title, url) + + if not title or not url: + logger.log(u"The XML returned from the "+self.provider.name+" feed is incomplete, this result is unusable", logger.ERROR) + return + + url = self._translateLinkURL(url) + + logger.log(u"Adding item from RSS to cache: "+title, logger.DEBUG) + + self._addCacheEntry(title, url) + + def _getLastUpdate(self): + myDB = self._getDB() + sqlResults = myDB.select("SELECT time FROM lastUpdate WHERE provider = ?", [self.providerID]) + + if sqlResults: + lastTime = int(sqlResults[0]["time"]) + else: + lastTime = 0 + + return datetime.datetime.fromtimestamp(lastTime) + + def setLastUpdate(self, toDate=None): + + if not toDate: + toDate = datetime.datetime.today() + + myDB = self._getDB() + myDB.upsert("lastUpdate", + {'time': int(time.mktime(toDate.timetuple()))}, + {'provider': self.providerID}) + + lastUpdate = property(_getLastUpdate) + + def shouldUpdate(self): + # if we've updated recently then skip the update + if datetime.datetime.today() - self.lastUpdate < datetime.timedelta(minutes=self.minTime): + logger.log(u"Last update was too soon, using old cache: today()-"+str(self.lastUpdate)+"<"+str(datetime.timedelta(minutes=self.minTime)), logger.DEBUG) + return False + + return True + + def _addCacheEntry(self, name, url, season=None, episodes=None, tvdb_id=0, tvrage_id=0, quality=None, extraNames=[]): + + myDB = self._getDB() + + parse_result = None + + # if we don't have complete info then parse the filename to get it + for curName in [name] + extraNames: + try: + myParser = NameParser() + parse_result = myParser.parse(curName) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+curName+" into a valid episode", logger.DEBUG) + continue + + if not parse_result: + logger.log(u"Giving up because I'm unable to parse this name: "+name, logger.DEBUG) + return False + + if not parse_result.series_name: + logger.log(u"No series name retrieved from "+name+", unable to cache it", logger.DEBUG) + return False + + tvdb_lang = None + + # if we need tvdb_id or tvrage_id then search the DB for them + if not tvdb_id or not tvrage_id: + + # if we have only the tvdb_id, use the database + if tvdb_id: + showObj = helpers.findCertainShow(sickbeard.showList, tvdb_id) + if showObj: + tvrage_id = showObj.tvrid + tvdb_lang = showObj.lang + else: + logger.log(u"We were given a TVDB id "+str(tvdb_id)+" but it doesn't match a show we have in our list, so leaving tvrage_id empty", logger.DEBUG) + tvrage_id = 0 + + # if we have only a tvrage_id then use the database + elif tvrage_id: + showObj = helpers.findCertainTVRageShow(sickbeard.showList, tvrage_id) + if showObj: + tvdb_id = showObj.tvdbid + tvdb_lang = showObj.lang + else: + logger.log(u"We were given a TVRage id "+str(tvrage_id)+" but it doesn't match a show we have in our list, so leaving tvdb_id empty", logger.DEBUG) + tvdb_id = 0 + + # if they're both empty then fill out as much info as possible by searching the show name + else: + + # check the name cache and see if we already know what show this is + logger.log(u"Checking the cache to see if we already know the tvdb id of "+parse_result.series_name, logger.DEBUG) + tvdb_id = name_cache.retrieveNameFromCache(parse_result.series_name) + + # remember if the cache lookup worked or not so we know whether we should bother updating it later + if tvdb_id == None: + logger.log(u"No cache results returned, continuing on with the search", logger.DEBUG) + from_cache = False + else: + logger.log(u"Cache lookup found "+repr(tvdb_id)+", using that", logger.DEBUG) + from_cache = True + + # if the cache failed, try looking up the show name in the database + if tvdb_id == None: + logger.log(u"Trying to look the show up in the show database", logger.DEBUG) + showResult = helpers.searchDBForShow(parse_result.series_name) + if showResult: + logger.log(parse_result.series_name+" was found to be show "+showResult[1]+" ("+str(showResult[0])+") in our DB.", logger.DEBUG) + tvdb_id = showResult[0] + + # if the DB lookup fails then do a comprehensive regex search + if tvdb_id == None: + logger.log(u"Couldn't figure out a show name straight from the DB, trying a regex search instead", logger.DEBUG) + for curShow in sickbeard.showList: + if show_name_helpers.isGoodResult(name, curShow, False): + logger.log(u"Successfully matched "+name+" to "+curShow.name+" with regex", logger.DEBUG) + tvdb_id = curShow.tvdbid + tvdb_lang = curShow.lang + break + + # if tvdb_id was anything but None (0 or a number) then + if not from_cache: + name_cache.addNameToCache(parse_result.series_name, tvdb_id) + + # if we came out with tvdb_id = None it means we couldn't figure it out at all, just use 0 for that + if tvdb_id == None: + tvdb_id = 0 + + # if we found the show then retrieve the show object + if tvdb_id: + showObj = helpers.findCertainShow(sickbeard.showList, tvdb_id) + if showObj: + tvrage_id = showObj.tvrid + tvdb_lang = showObj.lang + + # if we weren't provided with season/episode information then get it from the name that we parsed + if not season: + season = parse_result.season_number if parse_result.season_number != None else 1 + if not episodes: + episodes = parse_result.episode_numbers + + # if we have an air-by-date show then get the real season/episode numbers + if parse_result.air_by_date and tvdb_id: + try: + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if not (tvdb_lang == "" or tvdb_lang == "en" or tvdb_lang == None): + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + epObj = t[tvdb_id].airedOn(parse_result.air_date)[0] + season = int(epObj["seasonnumber"]) + episodes = [int(epObj["episodenumber"])] + except tvdb_exceptions.tvdb_episodenotfound: + logger.log(u"Unable to find episode with date "+str(parse_result.air_date)+" for show "+parse_result.series_name+", skipping", logger.WARNING) + return False + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB: "+ex(e), logger.WARNING) + return False + + episodeText = "|"+"|".join(map(str, episodes))+"|" + + # get the current timestamp + curTimestamp = int(time.mktime(datetime.datetime.today().timetuple())) + + if not quality: + quality = Quality.nameQuality(name) + + myDB.action("INSERT INTO "+self.providerID+" (name, season, episodes, tvrid, tvdbid, url, time, quality) VALUES (?,?,?,?,?,?,?,?)", + [name, season, episodeText, tvrage_id, tvdb_id, url, curTimestamp, quality]) + + + def searchCache(self, episode, manualSearch=False): + neededEps = self.findNeededEpisodes(episode, manualSearch) + return neededEps[episode] + + def listPropers(self, date=None, delimiter="."): + + myDB = self._getDB() + + sql = "SELECT * FROM "+self.providerID+" WHERE name LIKE '%.PROPER.%' OR name LIKE '%.REPACK.%'" + + if date != None: + sql += " AND time >= "+str(int(time.mktime(date.timetuple()))) + + #return filter(lambda x: x['tvdbid'] != 0, myDB.select(sql)) + return myDB.select(sql) + + def findNeededEpisodes(self, episode = None, manualSearch=False): + neededEps = {} + + if episode: + neededEps[episode] = [] + + myDB = self._getDB() + + if not episode: + sqlResults = myDB.select("SELECT * FROM "+self.providerID) + else: + sqlResults = myDB.select("SELECT * FROM "+self.providerID+" WHERE tvdbid = ? AND season = ? AND episodes LIKE ?", [episode.show.tvdbid, episode.season, "%|"+str(episode.episode)+"|%"]) + + # for each cache entry + for curResult in sqlResults: + + # get the show object, or if it's not one of our shows then ignore it + showObj = helpers.findCertainShow(sickbeard.showList, int(curResult["tvdbid"])) + if not showObj: + continue + + try: + fp = NameParser() + parse_result = fp.parse(curResult["name"]) + except InvalidNameException: + logger.log(u"Unable to parse the filename "+curResult["name"]+" into a valid episode", logger.WARNING) + + if not parse_result.series_language == showObj.audio_lang: + continue + + # skip non-tv crap (but allow them for Newzbin cause we assume it's filtered well) + if self.providerID != 'newzbin' and not show_name_helpers.filterBadReleases(curResult["name"],showObj.audio_lang): + continue + + # get season and ep data (ignoring multi-eps for now) + curSeason = int(curResult["season"]) + if curSeason == -1: + continue + curEp = curResult["episodes"].split("|")[1] + if not curEp: + continue + curEp = int(curEp) + curQuality = int(curResult["quality"]) + + # if the show says we want that episode then add it to the list + if not showObj.wantEpisode(curSeason, curEp, curQuality, manualSearch): + logger.log(u"Skipping "+curResult["name"]+" because we don't want an episode that's "+Quality.qualityStrings[curQuality], logger.DEBUG) + + else: + + if episode: + epObj = episode + else: + epObj = showObj.getEpisode(curSeason, curEp) + + # build a result object + title = curResult["name"] + url = curResult["url"] + + logger.log(u"Found result " + title + " at " + url) + + result = self.provider.getResult([epObj]) + result.url = url + result.name = title + result.quality = curQuality + result.audio_langs = [showObj.audio_lang] + + # add it to the list + if epObj not in neededEps: + neededEps[epObj] = [result] + else: + neededEps[epObj].append(result) + + return neededEps diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 766da1fc7ac1eea5f9751f8f91870e8006fd8c9b..f3550e734e1b62a1bfaf10886bcd79bfc7beaf1d 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -1,2863 +1,2909 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import os.path - -import time -import urllib -import re -import threading -import datetime -import random - -from Cheetah.Template import Template -import cherrypy.lib - -import sickbeard - -from sickbeard import config, sab -from sickbeard import history, notifiers, processTV -from sickbeard import ui -from sickbeard import logger, helpers, exceptions, classes, db -from sickbeard import encodingKludge as ek -from sickbeard import search_queue -from sickbeard import image_cache -from sickbeard import naming - -from sickbeard.providers import newznab -from sickbeard.common import Quality, Overview, statusStrings -from sickbeard.common import SNATCHED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED -from sickbeard.exceptions import ex -from sickbeard.webapi import Api - -from lib.tvdb_api import tvdb_api - -try: - import json -except ImportError: - from lib import simplejson as json - -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree - -from sickbeard import browser - - -class PageTemplate (Template): - def __init__(self, *args, **KWs): - KWs['file'] = os.path.join(sickbeard.PROG_DIR, "data/interfaces/default/", KWs['file']) - super(PageTemplate, self).__init__(*args, **KWs) - self.sbRoot = sickbeard.WEB_ROOT - self.sbHttpPort = sickbeard.WEB_PORT - self.sbHttpsPort = sickbeard.WEB_PORT - self.sbHttpsEnabled = sickbeard.ENABLE_HTTPS - if cherrypy.request.headers['Host'][0] == '[': - self.sbHost = re.match("^\[.*\]", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) - else: - self.sbHost = re.match("^[^:]+", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) - self.projectHomePage = "http://code.google.com/p/sickbeard/" - - if sickbeard.NZBS and sickbeard.NZBS_UID and sickbeard.NZBS_HASH: - logger.log(u"NZBs.org has been replaced, please check the config to configure the new provider!", logger.ERROR) - ui.notifications.error("NZBs.org Config Update", "NZBs.org has a new site. Please <a href=\""+sickbeard.WEB_ROOT+"/config/providers\">update your config</a> with the api key from <a href=\"http://beta.nzbs.org/login\">http://beta.nzbs.org</a> and then disable the old NZBs.org provider.") - - if "X-Forwarded-Host" in cherrypy.request.headers: - self.sbHost = cherrypy.request.headers['X-Forwarded-Host'] - if "X-Forwarded-Port" in cherrypy.request.headers: - self.sbHttpPort = cherrypy.request.headers['X-Forwarded-Port'] - self.sbHttpsPort = self.sbHttpPort - if "X-Forwarded-Proto" in cherrypy.request.headers: - self.sbHttpsEnabled = True if cherrypy.request.headers['X-Forwarded-Proto'] == 'https' else False - - logPageTitle = 'Logs & Errors' - if len(classes.ErrorViewer.errors): - logPageTitle += ' ('+str(len(classes.ErrorViewer.errors))+')' - self.logPageTitle = logPageTitle - self.sbPID = str(sickbeard.PID) - self.menu = [ - { 'title': 'Home', 'key': 'home' }, - { 'title': 'Coming Episodes', 'key': 'comingEpisodes' }, - { 'title': 'History', 'key': 'history' }, - { 'title': 'Manage', 'key': 'manage' }, - { 'title': 'Config', 'key': 'config' }, - { 'title': logPageTitle, 'key': 'errorlogs' }, - ] - -def redirect(abspath, *args, **KWs): - assert abspath[0] == '/' - raise cherrypy.HTTPRedirect(sickbeard.WEB_ROOT + abspath, *args, **KWs) - -class TVDBWebUI: - def __init__(self, config, log=None): - self.config = config - self.log = log - - def selectSeries(self, allSeries): - - searchList = ",".join([x['id'] for x in allSeries]) - showDirList = "" - for curShowDir in self.config['_showDir']: - showDirList += "showDir="+curShowDir+"&" - redirect("/home/addShows/addShow?" + showDirList + "seriesList=" + searchList) - -def _munge(string): - return unicode(string).encode('utf-8', 'xmlcharrefreplace') - -def _genericMessage(subject, message): - t = PageTemplate(file="genericMessage.tmpl") - t.submenu = HomeMenu() - t.subject = subject - t.message = message - return _munge(t) - -def _getEpisode(show, season, episode): - - if show == None or season == None or episode == None: - return "Invalid parameters" - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return "Show not in show list" - - epObj = showObj.getEpisode(int(season), int(episode)) - - if epObj == None: - return "Episode couldn't be retrieved" - - return epObj - -ManageMenu = [ - { 'title': 'Backlog Overview', 'path': 'manage/backlogOverview' }, - { 'title': 'Manage Searches', 'path': 'manage/manageSearches' }, - { 'title': 'Episode Status Management', 'path': 'manage/episodeStatuses' }, -] - -class ManageSearches: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="manage_manageSearches.tmpl") - #t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() - t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() #@UndefinedVariable - t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() #@UndefinedVariable - t.searchStatus = sickbeard.currentSearchScheduler.action.amActive #@UndefinedVariable - t.submenu = ManageMenu - - return _munge(t) - - @cherrypy.expose - def forceSearch(self): - - # force it to run the next time it looks - result = sickbeard.currentSearchScheduler.forceRun() - if result: - logger.log(u"Search forced") - ui.notifications.message('Episode search started', - 'Note: RSS feeds may not be updated if retrieved recently') - - redirect("/manage/manageSearches") - - @cherrypy.expose - def pauseBacklog(self, paused=None): - if paused == "1": - sickbeard.searchQueueScheduler.action.pause_backlog() #@UndefinedVariable - else: - sickbeard.searchQueueScheduler.action.unpause_backlog() #@UndefinedVariable - - redirect("/manage/manageSearches") - - @cherrypy.expose - def forceVersionCheck(self): - - # force a check to see if there is a new version - result = sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) #@UndefinedVariable - if result: - logger.log(u"Forcing version check") - - redirect("/manage/manageSearches") - - -class Manage: - - manageSearches = ManageSearches() - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="manage.tmpl") - t.submenu = ManageMenu - return _munge(t) - - @cherrypy.expose - def showEpisodeStatuses(self, tvdb_id, whichStatus): - myDB = db.DBConnection() - - status_list = [int(whichStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER - - cur_show_results = myDB.select("SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN ("+','.join(['?']*len(status_list))+")", [int(tvdb_id)] + status_list) - - result = {} - for cur_result in cur_show_results: - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) - - if cur_season not in result: - result[cur_season] = {} - - result[cur_season][cur_episode] = cur_result["name"] - - return json.dumps(result) - - @cherrypy.expose - def episodeStatuses(self, whichStatus=None): - - if whichStatus: - whichStatus = int(whichStatus) - status_list = [whichStatus] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER - else: - status_list = [] - - t = PageTemplate(file="manage_episodeStatuses.tmpl") - t.submenu = ManageMenu - t.whichStatus = whichStatus - - # if we have no status then this is as far as we need to go - if not status_list: - return _munge(t) - - myDB = db.DBConnection() - status_results = myDB.select("SELECT show_name, tv_shows.tvdb_id as tvdb_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND tv_episodes.showid = tv_shows.tvdb_id ORDER BY show_name", status_list) - - ep_counts = {} - show_names = {} - sorted_show_ids = [] - for cur_status_result in status_results: - cur_tvdb_id = int(cur_status_result["tvdb_id"]) - if cur_tvdb_id not in ep_counts: - ep_counts[cur_tvdb_id] = 1 - else: - ep_counts[cur_tvdb_id] += 1 - - show_names[cur_tvdb_id] = cur_status_result["show_name"] - if cur_tvdb_id not in sorted_show_ids: - sorted_show_ids.append(cur_tvdb_id) - - t.show_names = show_names - t.ep_counts = ep_counts - t.sorted_show_ids = sorted_show_ids - return _munge(t) - - @cherrypy.expose - def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): - - status_list = [int(oldStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER - - to_change = {} - - # make a list of all shows and their associated args - for arg in kwargs: - tvdb_id, what = arg.split('-') - - # we don't care about unchecked checkboxes - if kwargs[arg] != 'on': - continue - - if tvdb_id not in to_change: - to_change[tvdb_id] = [] - - to_change[tvdb_id].append(what) - - myDB = db.DBConnection() - - for cur_tvdb_id in to_change: - - # get a list of all the eps we want to change if they just said "all" - if 'all' in to_change[cur_tvdb_id]: - all_eps_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND showid = ?", status_list + [cur_tvdb_id]) - all_eps = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] - to_change[cur_tvdb_id] = all_eps - - Home().setStatus(cur_tvdb_id, '|'.join(to_change[cur_tvdb_id]), newStatus, direct=True) - - redirect('/manage/episodeStatuses') - - @cherrypy.expose - def backlogShow(self, tvdb_id): - - show_obj = helpers.findCertainShow(sickbeard.showList, int(tvdb_id)) - - if show_obj: - sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) #@UndefinedVariable - - redirect("/manage/backlogOverview") - - @cherrypy.expose - def backlogOverview(self): - - t = PageTemplate(file="manage_backlogOverview.tmpl") - t.submenu = ManageMenu - - myDB = db.DBConnection() - - showCounts = {} - showCats = {} - showSQLResults = {} - - for curShow in sickbeard.showList: - - epCounts = {} - epCats = {} - epCounts[Overview.SKIPPED] = 0 - epCounts[Overview.WANTED] = 0 - epCounts[Overview.QUAL] = 0 - epCounts[Overview.GOOD] = 0 - epCounts[Overview.UNAIRED] = 0 - - sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", [curShow.tvdbid]) - - for curResult in sqlResults: - - curEpCat = curShow.getOverview(int(curResult["status"])) - epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat - epCounts[curEpCat] += 1 - - showCounts[curShow.tvdbid] = epCounts - showCats[curShow.tvdbid] = epCats - showSQLResults[curShow.tvdbid] = sqlResults - - t.showCounts = showCounts - t.showCats = showCats - t.showSQLResults = showSQLResults - - return _munge(t) - - @cherrypy.expose - def massEdit(self, toEdit=None): - - t = PageTemplate(file="manage_massEdit.tmpl") - t.submenu = ManageMenu - - if not toEdit: - redirect("/manage") - - showIDs = toEdit.split("|") - showList = [] - for curID in showIDs: - curID = int(curID) - showObj = helpers.findCertainShow(sickbeard.showList, curID) - if showObj: - showList.append(showObj) - - flatten_folders_all_same = True - last_flatten_folders = None - - paused_all_same = True - last_paused = None - - quality_all_same = True - last_quality = None - - root_dir_list = [] - - for curShow in showList: - - cur_root_dir = ek.ek(os.path.dirname, curShow._location) - if cur_root_dir not in root_dir_list: - root_dir_list.append(cur_root_dir) - - # if we know they're not all the same then no point even bothering - if paused_all_same: - # if we had a value already and this value is different then they're not all the same - if last_paused not in (curShow.paused, None): - paused_all_same = False - else: - last_paused = curShow.paused - - if flatten_folders_all_same: - if last_flatten_folders not in (None, curShow.flatten_folders): - flatten_folders_all_same = False - else: - last_flatten_folders = curShow.flatten_folders - - if quality_all_same: - if last_quality not in (None, curShow.quality): - quality_all_same = False - else: - last_quality = curShow.quality - - t.showList = toEdit - t.paused_value = last_paused if paused_all_same else None - t.flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None - t.quality_value = last_quality if quality_all_same else None - t.root_dir_list = root_dir_list - - return _munge(t) - - @cherrypy.expose - def massEditSubmit(self, paused=None, flatten_folders=None, quality_preset=False, - anyQualities=[], bestQualities=[], toEdit=None, *args, **kwargs): - - dir_map = {} - for cur_arg in kwargs: - if not cur_arg.startswith('orig_root_dir_'): - continue - which_index = cur_arg.replace('orig_root_dir_', '') - end_dir = kwargs['new_root_dir_'+which_index] - dir_map[kwargs[cur_arg]] = end_dir - - showIDs = toEdit.split("|") - errors = [] - for curShow in showIDs: - curErrors = [] - showObj = helpers.findCertainShow(sickbeard.showList, int(curShow)) - if not showObj: - continue - - cur_root_dir = ek.ek(os.path.dirname, showObj._location) - cur_show_dir = ek.ek(os.path.basename, showObj._location) - if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: - new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) - logger.log(u"For show "+showObj.name+" changing dir from "+showObj._location+" to "+new_show_dir) - else: - new_show_dir = showObj._location - - if paused == 'keep': - new_paused = showObj.paused - else: - new_paused = True if paused == 'enable' else False - new_paused = 'on' if new_paused else 'off' - - if flatten_folders == 'keep': - new_flatten_folders = showObj.flatten_folders - else: - new_flatten_folders = True if flatten_folders == 'enable' else False - new_flatten_folders = 'on' if new_flatten_folders else 'off' - - if quality_preset == 'keep': - anyQualities, bestQualities = Quality.splitQuality(showObj.quality) - - curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, new_flatten_folders, new_paused, directCall=True) - - if curErrors: - logger.log(u"Errors: "+str(curErrors), logger.ERROR) - errors.append('<b>%s:</b>\n<ul>' % showObj.name + ' '.join(['<li>%s</li>' % error for error in curErrors]) + "</ul>") - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - " ".join(errors)) - - redirect("/manage") - - @cherrypy.expose - def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toMetadata=None): - - if toUpdate != None: - toUpdate = toUpdate.split('|') - else: - toUpdate = [] - - if toRefresh != None: - toRefresh = toRefresh.split('|') - else: - toRefresh = [] - - if toRename != None: - toRename = toRename.split('|') - else: - toRename = [] - - if toDelete != None: - toDelete = toDelete.split('|') - else: - toDelete = [] - - if toMetadata != None: - toMetadata = toMetadata.split('|') - else: - toMetadata = [] - - errors = [] - refreshes = [] - updates = [] - renames = [] - - for curShowID in set(toUpdate+toRefresh+toRename+toDelete+toMetadata): - - if curShowID == '': - continue - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(curShowID)) - - if showObj == None: - continue - - if curShowID in toDelete: - showObj.deleteShow() - # don't do anything else if it's being deleted - continue - - if curShowID in toUpdate: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable - updates.append(showObj.name) - except exceptions.CantUpdateException, e: - errors.append("Unable to update show "+showObj.name+": "+ex(e)) - - # don't bother refreshing shows that were updated anyway - if curShowID in toRefresh and curShowID not in toUpdate: - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - refreshes.append(showObj.name) - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh show "+showObj.name+": "+ex(e)) - - if curShowID in toRename: - sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) #@UndefinedVariable - renames.append(showObj.name) - - if len(errors) > 0: - ui.notifications.error("Errors encountered", - '<br >\n'.join(errors)) - - messageDetail = "" - - if len(updates) > 0: - messageDetail += "<br /><b>Updates</b><br /><ul><li>" - messageDetail += "</li><li>".join(updates) - messageDetail += "</li></ul>" - - if len(refreshes) > 0: - messageDetail += "<br /><b>Refreshes</b><br /><ul><li>" - messageDetail += "</li><li>".join(refreshes) - messageDetail += "</li></ul>" - - if len(renames) > 0: - messageDetail += "<br /><b>Renames</b><br /><ul><li>" - messageDetail += "</li><li>".join(renames) - messageDetail += "</li></ul>" - - if len(updates+refreshes+renames) > 0: - ui.notifications.message("The following actions were queued:", - messageDetail) - - redirect("/manage") - - -class History: - - @cherrypy.expose - def index(self, limit=100): - - myDB = db.DBConnection() - -# sqlResults = myDB.select("SELECT h.*, show_name, name FROM history h, tv_shows s, tv_episodes e WHERE h.showid=s.tvdb_id AND h.showid=e.showid AND h.season=e.season AND h.episode=e.episode ORDER BY date DESC LIMIT "+str(numPerPage*(p-1))+", "+str(numPerPage)) - if limit == "0": - sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC") - else: - sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC LIMIT ?", [limit]) - - t = PageTemplate(file="history.tmpl") - t.historyResults = sqlResults - t.limit = limit - t.submenu = [ - { 'title': 'Clear History', 'path': 'history/clearHistory' }, - { 'title': 'Trim History', 'path': 'history/trimHistory' }, - ] - - return _munge(t) - - - @cherrypy.expose - def clearHistory(self): - - myDB = db.DBConnection() - myDB.action("DELETE FROM history WHERE 1=1") - ui.notifications.message('History cleared') - redirect("/history") - - - @cherrypy.expose - def trimHistory(self): - - myDB = db.DBConnection() - myDB.action("DELETE FROM history WHERE date < "+str((datetime.datetime.today()-datetime.timedelta(days=30)).strftime(history.dateFormat))) - ui.notifications.message('Removed history entries greater than 30 days old') - redirect("/history") - - -ConfigMenu = [ - { 'title': 'General', 'path': 'config/general/' }, - { 'title': 'Search Settings', 'path': 'config/search/' }, - { 'title': 'Search Providers', 'path': 'config/providers/' }, - { 'title': 'Post Processing', 'path': 'config/postProcessing/' }, - { 'title': 'Notifications', 'path': 'config/notifications/' }, -] - -class ConfigGeneral: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config_general.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveRootDirs(self, rootDirString=None): - sickbeard.ROOT_DIRS = rootDirString - - @cherrypy.expose - def saveAddShowDefaults(self, defaultFlattenFolders, defaultStatus, anyQualities, bestQualities): - - if anyQualities: - anyQualities = anyQualities.split(',') - else: - anyQualities = [] - - if bestQualities: - bestQualities = bestQualities.split(',') - else: - bestQualities = [] - - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - - sickbeard.STATUS_DEFAULT = int(defaultStatus) - sickbeard.QUALITY_DEFAULT = int(newQuality) - - if defaultFlattenFolders == "true": - defaultFlattenFolders = 1 - else: - defaultFlattenFolders = 0 - - sickbeard.FLATTEN_FOLDERS_DEFAULT = int(defaultFlattenFolders) - - @cherrypy.expose - def generateKey(self): - """ Return a new randomized API_KEY - """ - - try: - from hashlib import md5 - except ImportError: - from md5 import md5 - - # Create some values to seed md5 - t = str(time.time()) - r = str(random.random()) - - # Create the md5 instance and give it the current time - m = md5(t) - - # Update the md5 instance with the random variable - m.update(r) - - # Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b - logger.log(u"New API generated") - return m.hexdigest() - - @cherrypy.expose - def saveGeneral(self, log_dir=None, web_port=None, web_log=None, web_ipv6=None, - launch_browser=None, web_username=None, use_api=None, api_key=None, - web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None): - - results = [] - - if web_ipv6 == "on": - web_ipv6 = 1 - else: - web_ipv6 = 0 - - if web_log == "on": - web_log = 1 - else: - web_log = 0 - - if launch_browser == "on": - launch_browser = 1 - else: - launch_browser = 0 - - if version_notify == "on": - version_notify = 1 - else: - version_notify = 0 - - if not config.change_LOG_DIR(log_dir): - results += ["Unable to create directory " + os.path.normpath(log_dir) + ", log dir not changed."] - - sickbeard.LAUNCH_BROWSER = launch_browser - - sickbeard.WEB_PORT = int(web_port) - sickbeard.WEB_IPV6 = web_ipv6 - sickbeard.WEB_LOG = web_log - sickbeard.WEB_USERNAME = web_username - sickbeard.WEB_PASSWORD = web_password - - if use_api == "on": - use_api = 1 - else: - use_api = 0 - - sickbeard.USE_API = use_api - sickbeard.API_KEY = api_key - - if enable_https == "on": - enable_https = 1 - else: - enable_https = 0 - - sickbeard.ENABLE_HTTPS = enable_https - - if not config.change_HTTPS_CERT(https_cert): - results += ["Unable to create directory " + os.path.normpath(https_cert) + ", https cert dir not changed."] - - if not config.change_HTTPS_KEY(https_key): - results += ["Unable to create directory " + os.path.normpath(https_key) + ", https key dir not changed."] - - config.change_VERSION_NOTIFY(version_notify) - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/general/") - - -class ConfigSearch: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config_search.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, - sab_apikey=None, sab_category=None, sab_host=None, nzbget_password=None, nzbget_category=None, nzbget_host=None, - torrent_dir=None, nzb_method=None, usenet_retention=None, search_frequency=None, download_propers=None): - - results = [] - - if not config.change_NZB_DIR(nzb_dir): - results += ["Unable to create directory " + os.path.normpath(nzb_dir) + ", dir not changed."] - - if not config.change_TORRENT_DIR(torrent_dir): - results += ["Unable to create directory " + os.path.normpath(torrent_dir) + ", dir not changed."] - - config.change_SEARCH_FREQUENCY(search_frequency) - - if download_propers == "on": - download_propers = 1 - else: - download_propers = 0 - - if use_nzbs == "on": - use_nzbs = 1 - else: - use_nzbs = 0 - - if use_torrents == "on": - use_torrents = 1 - else: - use_torrents = 0 - - if usenet_retention == None: - usenet_retention = 200 - - sickbeard.USE_NZBS = use_nzbs - sickbeard.USE_TORRENTS = use_torrents - - sickbeard.NZB_METHOD = nzb_method - sickbeard.USENET_RETENTION = int(usenet_retention) - - sickbeard.DOWNLOAD_PROPERS = download_propers - - sickbeard.SAB_USERNAME = sab_username - sickbeard.SAB_PASSWORD = sab_password - sickbeard.SAB_APIKEY = sab_apikey.strip() - sickbeard.SAB_CATEGORY = sab_category - - if sab_host and not re.match('https?://.*', sab_host): - sab_host = 'http://' + sab_host - - if not sab_host.endswith('/'): - sab_host = sab_host + '/' - - sickbeard.SAB_HOST = sab_host - - sickbeard.NZBGET_PASSWORD = nzbget_password - sickbeard.NZBGET_CATEGORY = nzbget_category - sickbeard.NZBGET_HOST = nzbget_host - - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/search/") - -class ConfigPostProcessing: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config_postProcessing.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, - xbmc_data=None, mediabrowser_data=None, synology_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, - use_banner=None, keep_processed_dir=None, process_automatically=None, rename_episodes=None, - move_associated_files=None, tv_download_dir=None, naming_custom_abd=None, naming_abd_pattern=None): - - results = [] - - if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): - results += ["Unable to create directory " + os.path.normpath(tv_download_dir) + ", dir not changed."] - - if use_banner == "on": - use_banner = 1 - else: - use_banner = 0 - - if process_automatically == "on": - process_automatically = 1 - else: - process_automatically = 0 - - if rename_episodes == "on": - rename_episodes = 1 - else: - rename_episodes = 0 - - if keep_processed_dir == "on": - keep_processed_dir = 1 - else: - keep_processed_dir = 0 - - if move_associated_files == "on": - move_associated_files = 1 - else: - move_associated_files = 0 - - if naming_custom_abd == "on": - naming_custom_abd = 1 - else: - naming_custom_abd = 0 - - sickbeard.PROCESS_AUTOMATICALLY = process_automatically - sickbeard.KEEP_PROCESSED_DIR = keep_processed_dir - sickbeard.RENAME_EPISODES = rename_episodes - sickbeard.MOVE_ASSOCIATED_FILES = move_associated_files - sickbeard.NAMING_CUSTOM_ABD = naming_custom_abd - - sickbeard.metadata_provider_dict['XBMC'].set_config(xbmc_data) - sickbeard.metadata_provider_dict['MediaBrowser'].set_config(mediabrowser_data) - sickbeard.metadata_provider_dict['Synology'].set_config(synology_data) - sickbeard.metadata_provider_dict['Sony PS3'].set_config(sony_ps3_data) - sickbeard.metadata_provider_dict['WDTV'].set_config(wdtv_data) - sickbeard.metadata_provider_dict['TIVO'].set_config(tivo_data) - - if self.isNamingValid(naming_pattern, naming_multi_ep) != "invalid": - sickbeard.NAMING_PATTERN = naming_pattern - sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) - sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() - else: - results.append("You tried saving an invalid naming config, not saving your naming settings") - - if self.isNamingValid(naming_abd_pattern, None, True) != "invalid": - sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern - elif naming_custom_abd: - results.append("You tried saving an invalid air-by-date naming config, not saving your air-by-date settings") - - sickbeard.USE_BANNER = use_banner - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/postProcessing/") - - @cherrypy.expose - def testNaming(self, pattern=None, multi=None, abd=False): - - if multi != None: - multi = int(multi) - - result = naming.test_name(pattern, multi, abd) - - result = ek.ek(os.path.join, result['dir'], result['name']) - - return result - - @cherrypy.expose - def isNamingValid(self, pattern=None, multi=None, abd=False): - if pattern == None: - return "invalid" - - # air by date shows just need one check, we don't need to worry about season folders - if abd: - is_valid = naming.check_valid_abd_naming(pattern) - require_season_folders = False - - else: - # check validity of single and multi ep cases for the whole path - is_valid = naming.check_valid_naming(pattern, multi) - - # check validity of single and multi ep cases for only the file name - require_season_folders = naming.check_force_season_folders(pattern, multi) - - if is_valid and not require_season_folders: - return "valid" - elif is_valid and require_season_folders: - return "seasonfolders" - else: - return "invalid" - - -class ConfigProviders: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="config_providers.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def canAddNewznabProvider(self, name): - - if not name: - return json.dumps({'error': 'Invalid name specified'}) - - providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - tempProvider = newznab.NewznabProvider(name, '') - - if tempProvider.getID() in providerDict: - return json.dumps({'error': 'Exists as '+providerDict[tempProvider.getID()].name}) - else: - return json.dumps({'success': tempProvider.getID()}) - - @cherrypy.expose - def saveNewznabProvider(self, name, url, key=''): - - if not name or not url: - return '0' - - if not url.endswith('/'): - url = url + '/' - - providerDict = dict(zip([x.name for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - if name in providerDict: - if not providerDict[name].default: - providerDict[name].name = name - providerDict[name].url = url - providerDict[name].key = key - - return providerDict[name].getID() + '|' + providerDict[name].configStr() - - else: - - newProvider = newznab.NewznabProvider(name, url, key) - sickbeard.newznabProviderList.append(newProvider) - return newProvider.getID() + '|' + newProvider.configStr() - - - - @cherrypy.expose - def deleteNewznabProvider(self, id): - - providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - if id not in providerDict or providerDict[id].default: - return '0' - - # delete it from the list - sickbeard.newznabProviderList.remove(providerDict[id]) - - if id in sickbeard.PROVIDER_ORDER: - sickbeard.PROVIDER_ORDER.remove(id) - - return '1' - - - @cherrypy.expose - def saveProviders(self, nzbmatrix_username=None, nzbmatrix_apikey=None, - nzbs_r_us_uid=None, nzbs_r_us_hash=None, newznab_string=None, - tvtorrents_digest=None, tvtorrents_hash=None, - btn_api_key=None, binnewz_language=None, - newzbin_username=None, newzbin_password=None,t411_language=None,t411_username=None,t411_password=None, - provider_order=None): - - results = [] - - provider_str_list = provider_order.split() - provider_list = [] - - newznabProviderDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - finishedNames = [] - - # add all the newznab info we got into our list - for curNewznabProviderStr in newznab_string.split('!!!'): - - if not curNewznabProviderStr: - continue - - curName, curURL, curKey = curNewznabProviderStr.split('|') - - newProvider = newznab.NewznabProvider(curName, curURL, curKey) - - curID = newProvider.getID() - - # if it already exists then update it - if curID in newznabProviderDict: - newznabProviderDict[curID].name = curName - newznabProviderDict[curID].url = curURL - newznabProviderDict[curID].key = curKey - else: - sickbeard.newznabProviderList.append(newProvider) - - finishedNames.append(curID) - - # delete anything that is missing - for curProvider in sickbeard.newznabProviderList: - if curProvider.getID() not in finishedNames: - sickbeard.newznabProviderList.remove(curProvider) - - # do the enable/disable - for curProviderStr in provider_str_list: - curProvider, curEnabled = curProviderStr.split(':') - curEnabled = int(curEnabled) - - provider_list.append(curProvider) - - if curProvider == 'nzbs_r_us': - sickbeard.NZBSRUS = curEnabled - elif curProvider == 'nzbs_org_old': - sickbeard.NZBS = curEnabled - elif curProvider == 'nzbmatrix': - sickbeard.NZBMATRIX = curEnabled - elif curProvider == 'newzbin': - sickbeard.NEWZBIN = curEnabled - elif curProvider == 'bin_req': - sickbeard.BINREQ = curEnabled - elif curProvider == 'womble_s_index': - sickbeard.WOMBLE = curEnabled - elif curProvider == 'ezrss': - sickbeard.EZRSS = curEnabled - elif curProvider == 'tvtorrents': - sickbeard.TVTORRENTS = curEnabled - elif curProvider == 'btn': - sickbeard.BTN = curEnabled - elif curProvider == 'binnewz': - sickbeard.BINNEWZ = curEnabled - elif curProvider == 't411': - sickbeard.T411 = curEnabled - elif curProvider in newznabProviderDict: - newznabProviderDict[curProvider].enabled = bool(curEnabled) - else: - logger.log(u"don't know what "+curProvider+" is, skipping") - - sickbeard.TVTORRENTS_DIGEST = tvtorrents_digest.strip() - sickbeard.TVTORRENTS_HASH = tvtorrents_hash.strip() - - sickbeard.BTN_API_KEY = btn_api_key.strip() - - sickbeard.BINNEWZ_LANGUAGE = binnewz_language - - sickbeard.T411_LANGUAGE = t411_language - sickbeard.T411_USERNAME = t411_username - sickbeard.T411_PASSWORD = t411_password - - sickbeard.NZBSRUS_UID = nzbs_r_us_uid.strip() - sickbeard.NZBSRUS_HASH = nzbs_r_us_hash.strip() - - sickbeard.PROVIDER_ORDER = provider_list - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/providers/") - -class ConfigNotifications: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="config_notifications.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveNotifications(self, use_xbmc=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, - xbmc_update_library=None, xbmc_update_full=None, xbmc_host=None, xbmc_username=None, xbmc_password=None, - use_plex=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, plex_update_library=None, - plex_server_host=None, plex_host=None, plex_username=None, plex_password=None, - use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, growl_host=None, growl_password=None, - use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, prowl_api=None, prowl_priority=0, - use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, - use_notifo=None, notifo_notify_onsnatch=None, notifo_notify_ondownload=None, notifo_username=None, notifo_apisecret=None, - use_boxcar=None, boxcar_notify_onsnatch=None, boxcar_notify_ondownload=None, boxcar_username=None, - use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, pushover_userkey=None, - use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, - use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, - use_trakt=None, trakt_username=None, trakt_password=None, trakt_api=None, - use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, pytivo_update_library=None, - pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, - use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, nma_api=None, nma_priority=0 ): - - results = [] - - if xbmc_notify_onsnatch == "on": - xbmc_notify_onsnatch = 1 - else: - xbmc_notify_onsnatch = 0 - - if xbmc_notify_ondownload == "on": - xbmc_notify_ondownload = 1 - else: - xbmc_notify_ondownload = 0 - - if xbmc_update_library == "on": - xbmc_update_library = 1 - else: - xbmc_update_library = 0 - - if xbmc_update_full == "on": - xbmc_update_full = 1 - else: - xbmc_update_full = 0 - - if use_xbmc == "on": - use_xbmc = 1 - else: - use_xbmc = 0 - - if plex_update_library == "on": - plex_update_library = 1 - else: - plex_update_library = 0 - - if plex_notify_onsnatch == "on": - plex_notify_onsnatch = 1 - else: - plex_notify_onsnatch = 0 - - if plex_notify_ondownload == "on": - plex_notify_ondownload = 1 - else: - plex_notify_ondownload = 0 - - if use_plex == "on": - use_plex = 1 - else: - use_plex = 0 - - if growl_notify_onsnatch == "on": - growl_notify_onsnatch = 1 - else: - growl_notify_onsnatch = 0 - - if growl_notify_ondownload == "on": - growl_notify_ondownload = 1 - else: - growl_notify_ondownload = 0 - - if use_growl == "on": - use_growl = 1 - else: - use_growl = 0 - - if prowl_notify_onsnatch == "on": - prowl_notify_onsnatch = 1 - else: - prowl_notify_onsnatch = 0 - - if prowl_notify_ondownload == "on": - prowl_notify_ondownload = 1 - else: - prowl_notify_ondownload = 0 - if use_prowl == "on": - use_prowl = 1 - else: - use_prowl = 0 - - if twitter_notify_onsnatch == "on": - twitter_notify_onsnatch = 1 - else: - twitter_notify_onsnatch = 0 - - if twitter_notify_ondownload == "on": - twitter_notify_ondownload = 1 - else: - twitter_notify_ondownload = 0 - if use_twitter == "on": - use_twitter = 1 - else: - use_twitter = 0 - - if notifo_notify_onsnatch == "on": - notifo_notify_onsnatch = 1 - else: - notifo_notify_onsnatch = 0 - - if notifo_notify_ondownload == "on": - notifo_notify_ondownload = 1 - else: - notifo_notify_ondownload = 0 - if use_notifo == "on": - use_notifo = 1 - else: - use_notifo = 0 - - if boxcar_notify_onsnatch == "on": - boxcar_notify_onsnatch = 1 - else: - boxcar_notify_onsnatch = 0 - - if boxcar_notify_ondownload == "on": - boxcar_notify_ondownload = 1 - else: - boxcar_notify_ondownload = 0 - if use_boxcar == "on": - use_boxcar = 1 - else: - use_boxcar = 0 - - if pushover_notify_onsnatch == "on": - pushover_notify_onsnatch = 1 - else: - pushover_notify_onsnatch = 0 - - if pushover_notify_ondownload == "on": - pushover_notify_ondownload = 1 - else: - pushover_notify_ondownload = 0 - if use_pushover == "on": - use_pushover = 1 - else: - use_pushover = 0 - - if use_nmj == "on": - use_nmj = 1 - else: - use_nmj = 0 - - if use_synoindex == "on": - use_synoindex = 1 - else: - use_synoindex = 0 - - if use_trakt == "on": - use_trakt = 1 - else: - use_trakt = 0 - - if use_pytivo == "on": - use_pytivo = 1 - else: - use_pytivo = 0 - - if pytivo_notify_onsnatch == "on": - pytivo_notify_onsnatch = 1 - else: - pytivo_notify_onsnatch = 0 - - if pytivo_notify_ondownload == "on": - pytivo_notify_ondownload = 1 - else: - pytivo_notify_ondownload = 0 - - if pytivo_update_library == "on": - pytivo_update_library = 1 - else: - pytivo_update_library = 0 - - if use_nma == "on": - use_nma = 1 - else: - use_nma = 0 - - if nma_notify_onsnatch == "on": - nma_notify_onsnatch = 1 - else: - nma_notify_onsnatch = 0 - - if nma_notify_ondownload == "on": - nma_notify_ondownload = 1 - else: - nma_notify_ondownload = 0 - - sickbeard.USE_XBMC = use_xbmc - sickbeard.XBMC_NOTIFY_ONSNATCH = xbmc_notify_onsnatch - sickbeard.XBMC_NOTIFY_ONDOWNLOAD = xbmc_notify_ondownload - sickbeard.XBMC_UPDATE_LIBRARY = xbmc_update_library - sickbeard.XBMC_UPDATE_FULL = xbmc_update_full - sickbeard.XBMC_HOST = xbmc_host - sickbeard.XBMC_USERNAME = xbmc_username - sickbeard.XBMC_PASSWORD = xbmc_password - - sickbeard.USE_PLEX = use_plex - sickbeard.PLEX_NOTIFY_ONSNATCH = plex_notify_onsnatch - sickbeard.PLEX_NOTIFY_ONDOWNLOAD = plex_notify_ondownload - sickbeard.PLEX_UPDATE_LIBRARY = plex_update_library - sickbeard.PLEX_HOST = plex_host - sickbeard.PLEX_SERVER_HOST = plex_server_host - sickbeard.PLEX_USERNAME = plex_username - sickbeard.PLEX_PASSWORD = plex_password - - sickbeard.USE_GROWL = use_growl - sickbeard.GROWL_NOTIFY_ONSNATCH = growl_notify_onsnatch - sickbeard.GROWL_NOTIFY_ONDOWNLOAD = growl_notify_ondownload - sickbeard.GROWL_HOST = growl_host - sickbeard.GROWL_PASSWORD = growl_password - - sickbeard.USE_PROWL = use_prowl - sickbeard.PROWL_NOTIFY_ONSNATCH = prowl_notify_onsnatch - sickbeard.PROWL_NOTIFY_ONDOWNLOAD = prowl_notify_ondownload - sickbeard.PROWL_API = prowl_api - sickbeard.PROWL_PRIORITY = prowl_priority - - sickbeard.USE_TWITTER = use_twitter - sickbeard.TWITTER_NOTIFY_ONSNATCH = twitter_notify_onsnatch - sickbeard.TWITTER_NOTIFY_ONDOWNLOAD = twitter_notify_ondownload - - sickbeard.USE_NOTIFO = use_notifo - sickbeard.NOTIFO_NOTIFY_ONSNATCH = notifo_notify_onsnatch - sickbeard.NOTIFO_NOTIFY_ONDOWNLOAD = notifo_notify_ondownload - sickbeard.NOTIFO_USERNAME = notifo_username - sickbeard.NOTIFO_APISECRET = notifo_apisecret - - sickbeard.USE_BOXCAR = use_boxcar - sickbeard.BOXCAR_NOTIFY_ONSNATCH = boxcar_notify_onsnatch - sickbeard.BOXCAR_NOTIFY_ONDOWNLOAD = boxcar_notify_ondownload - sickbeard.BOXCAR_USERNAME = boxcar_username - - sickbeard.USE_PUSHOVER = use_pushover - sickbeard.PUSHOVER_NOTIFY_ONSNATCH = pushover_notify_onsnatch - sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD = pushover_notify_ondownload - sickbeard.PUSHOVER_USERKEY = pushover_userkey - - sickbeard.USE_LIBNOTIFY = use_libnotify == "on" - sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH = libnotify_notify_onsnatch == "on" - sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD = libnotify_notify_ondownload == "on" - - sickbeard.USE_NMJ = use_nmj - sickbeard.NMJ_HOST = nmj_host - sickbeard.NMJ_DATABASE = nmj_database - sickbeard.NMJ_MOUNT = nmj_mount - - sickbeard.USE_SYNOINDEX = use_synoindex - - sickbeard.USE_TRAKT = use_trakt - sickbeard.TRAKT_USERNAME = trakt_username - sickbeard.TRAKT_PASSWORD = trakt_password - sickbeard.TRAKT_API = trakt_api - - sickbeard.USE_PYTIVO = use_pytivo - sickbeard.PYTIVO_NOTIFY_ONSNATCH = pytivo_notify_onsnatch == "off" - sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = pytivo_notify_ondownload == "off" - sickbeard.PYTIVO_UPDATE_LIBRARY = pytivo_update_library - sickbeard.PYTIVO_HOST = pytivo_host - sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name - sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name - - sickbeard.USE_NMA = use_nma - sickbeard.NMA_NOTIFY_ONSNATCH = nma_notify_onsnatch - sickbeard.NMA_NOTIFY_ONDOWNLOAD = nma_notify_ondownload - sickbeard.NMA_API = nma_api - sickbeard.NMA_PRIORITY = nma_priority - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/notifications/") - - -class Config: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - general = ConfigGeneral() - - search = ConfigSearch() - - postProcessing = ConfigPostProcessing() - - providers = ConfigProviders() - - notifications = ConfigNotifications() - -def haveXBMC(): - return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY - -def havePLEX(): - return sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY - -def HomeMenu(): - return [ - { 'title': 'Add Shows', 'path': 'home/addShows/', }, - { 'title': 'Manual Post-Processing', 'path': 'home/postprocess/' }, - { 'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': haveXBMC }, - { 'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': havePLEX }, - { 'title': 'Restart', 'path': 'home/restart/?pid='+str(sickbeard.PID), 'confirm': True }, - { 'title': 'Shutdown', 'path': 'home/shutdown/?pid='+str(sickbeard.PID), 'confirm': True }, - ] - -class HomePostProcess: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="home_postprocess.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - @cherrypy.expose - def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None): - - if not dir: - redirect("/home/postprocess") - else: - result = processTV.processDir(dir, nzbName) - if quiet != None and int(quiet) == 1: - return result - - result = result.replace("\n","<br />\n") - return _genericMessage("Postprocessing results", result) - - -class NewHomeAddShows: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="home_addShows.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - @cherrypy.expose - def getTVDBLanguages(self): - result = tvdb_api.Tvdb().config['valid_languages'] - - # Make sure list is sorted alphabetically but 'en' is in front - if 'en' in result: - del result[result.index('en')] - result.sort() - result.insert(0, 'en') - - return json.dumps({'results': result}) - - @cherrypy.expose - def sanitizeFileName(self, name): - return helpers.sanitizeFileName(name) - - @cherrypy.expose - def searchTVDBForShowName(self, name, lang="en"): - if not lang or lang == 'null': - lang = "en" - - baseURL = "http://www.thetvdb.com/api/GetSeries.php?" - nameUTF8 = name.encode('utf-8') - - logger.log(u"Trying to find Show on thetvdb.com with: " + nameUTF8.decode('utf-8'), logger.DEBUG) - - # Use each word in the show's name as a possible search term - keywords = nameUTF8.split(' ') - - # Insert the whole show's name as the first search term so best results are first - # ex: keywords = ['Some Show Name', 'Some', 'Show', 'Name'] - if len(keywords) > 1: - keywords.insert(0, nameUTF8) - - # Query the TVDB for each search term and build the list of results - results = [] - - for searchTerm in keywords: - params = {'seriesname': searchTerm, - 'language': lang} - - finalURL = baseURL + urllib.urlencode(params) - - logger.log(u"Searching for Show with searchterm: \'" + searchTerm.decode('utf-8') + u"\' on URL " + finalURL, logger.DEBUG) - urlData = helpers.getURL(finalURL) - - if urlData is None: - # When urlData is None, trouble connecting to TVDB, don't try the rest of the keywords - logger.log(u"Unable to get URL: " + finalURL, logger.ERROR) - break - else: - try: - seriesXML = etree.ElementTree(etree.XML(urlData)) - series = seriesXML.getiterator('Series') - - except Exception, e: - # use finalURL in log, because urlData can be too much information - logger.log(u"Unable to parse XML for some reason: " + ex(e) + " from XML: " + finalURL, logger.ERROR) - series = '' - - # add each result to our list - for curSeries in series: - tvdb_id = int(curSeries.findtext('seriesid')) - - # don't add duplicates - if tvdb_id in [x[0] for x in results]: - continue - - results.append((tvdb_id, curSeries.findtext('SeriesName'), curSeries.findtext('FirstAired'))) - - lang_id = tvdb_api.Tvdb().config['langabbv_to_id'][lang] - - return json.dumps({'results': results, 'langid': lang_id}) - - @cherrypy.expose - def massAddTable(self, rootDir=None): - t = PageTemplate(file="home_massAddTable.tmpl") - t.submenu = HomeMenu() - - myDB = db.DBConnection() - - if not rootDir: - return "No folders selected." - elif type(rootDir) != list: - root_dirs = [rootDir] - else: - root_dirs = rootDir - - root_dirs = [urllib.unquote_plus(x) for x in root_dirs] - - default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) - if len(root_dirs) > default_index: - tmp = root_dirs[default_index] - if tmp in root_dirs: - root_dirs.remove(tmp) - root_dirs = [tmp]+root_dirs - - dir_list = [] - - for root_dir in root_dirs: - try: - file_list = ek.ek(os.listdir, root_dir) - except: - continue - - for cur_file in file_list: - - cur_path = ek.ek(os.path.normpath, ek.ek(os.path.join, root_dir, cur_file)) - if not ek.ek(os.path.isdir, cur_path): - continue - - cur_dir = { - 'dir': cur_path, - 'display_dir': '<b>'+ek.ek(os.path.dirname, cur_path)+os.sep+'</b>'+ek.ek(os.path.basename, cur_path), - } - - # see if the folder is in XBMC already - dirResults = myDB.select("SELECT * FROM tv_shows WHERE location = ?", [cur_path]) - - if dirResults: - cur_dir['added_already'] = True - else: - cur_dir['added_already'] = False - - dir_list.append(cur_dir) - - tvdb_id = '' - show_name = '' - for cur_provider in sickbeard.metadata_provider_dict.values(): - (tvdb_id, show_name) = cur_provider.retrieveShowMetadata(cur_path) - if tvdb_id and show_name: - break - - cur_dir['existing_info'] = (tvdb_id, show_name) - - if tvdb_id and helpers.findCertainShow(sickbeard.showList, tvdb_id): - cur_dir['added_already'] = True - - t.dirList = dir_list - - return _munge(t) - - @cherrypy.expose - def newShow(self, show_to_add=None, other_shows=None): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - t = PageTemplate(file="home_newShow.tmpl") - t.submenu = HomeMenu() - - show_dir, tvdb_id, show_name = self.split_extra_show(show_to_add) - - if tvdb_id and show_name: - use_provided_info = True - else: - use_provided_info = False - - # tell the template whether we're giving it show name & TVDB ID - t.use_provided_info = use_provided_info - - # use the given show_dir for the tvdb search if available - if not show_dir: - t.default_show_name = '' - elif not show_name: - t.default_show_name = ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.',' ') - else: - t.default_show_name = show_name - - # carry a list of other dirs if given - if not other_shows: - other_shows = [] - elif type(other_shows) != list: - other_shows = [other_shows] - - if use_provided_info: - t.provided_tvdb_id = tvdb_id - t.provided_tvdb_name = show_name - - t.provided_show_dir = show_dir - t.other_shows = other_shows - - return _munge(t) - - @cherrypy.expose - def addNewShow(self, whichSeries=None, tvdbLang="en", rootDir=None, defaultStatus=None, - anyQualities=None, bestQualities=None, flatten_folders=None, fullShowPath=None, - other_shows=None, skipShow=None): - """ - Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are - provided then it forwards back to newShow, if not it goes to /home. - """ - - # grab our list of other dirs if given - if not other_shows: - other_shows = [] - elif type(other_shows) != list: - other_shows = [other_shows] - - def finishAddShow(): - # if there are no extra shows then go home - if not other_shows: - redirect('/home') - - # peel off the next one - next_show_dir = other_shows[0] - rest_of_show_dirs = other_shows[1:] - - # go to add the next show - return self.newShow(next_show_dir, rest_of_show_dirs) - - # if we're skipping then behave accordingly - if skipShow: - return finishAddShow() - - # sanity check on our inputs - if (not rootDir and not fullShowPath) or not whichSeries: - return "Missing params, no tvdb id or folder:"+repr(whichSeries)+" and "+repr(rootDir)+"/"+repr(fullShowPath) - - # figure out what show we're adding and where - series_pieces = whichSeries.partition('|') - if len(series_pieces) < 3: - return "Error with show selection." - - tvdb_id = int(series_pieces[0]) - show_name = series_pieces[2] - - # use the whole path if it's given, or else append the show name to the root dir to get the full show path - if fullShowPath: - show_dir = ek.ek(os.path.normpath, fullShowPath) - else: - show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) - - # blanket policy - if the dir exists you should have used "add existing show" numbnuts - if ek.ek(os.path.isdir, show_dir) and not fullShowPath: - ui.notifications.error("Unable to add show", "Folder "+show_dir+" exists already") - redirect('/home/addShows/existingShows') - - # don't create show dir if config says not to - if sickbeard.ADD_SHOWS_WO_DIR: - logger.log(u"Skipping initial creation of "+show_dir+" due to config.ini setting") - else: - dir_exists = helpers.makeDir(show_dir) - if not dir_exists: - logger.log(u"Unable to create the folder "+show_dir+", can't add the show", logger.ERROR) - ui.notifications.error("Unable to add show", "Unable to create the folder "+show_dir+", can't add the show") - redirect("/home") - else: - helpers.chmodAsParent(show_dir) - - # prepare the inputs for passing along - if flatten_folders == "on": - flatten_folders = 1 - else: - flatten_folders = 0 - - if not anyQualities: - anyQualities = [] - if not bestQualities: - bestQualities = [] - if type(anyQualities) != list: - anyQualities = [anyQualities] - if type(bestQualities) != list: - bestQualities = [bestQualities] - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - - # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, flatten_folders, tvdbLang) #@UndefinedVariable - ui.notifications.message('Show added', 'Adding the specified show into '+show_dir) - - return finishAddShow() - - - @cherrypy.expose - def existingShows(self): - """ - Prints out the page to add existing shows from a root dir - """ - t = PageTemplate(file="home_addExistingShow.tmpl") - t.submenu = HomeMenu() - - return _munge(t) - - def split_extra_show(self, extra_show): - if not extra_show: - return (None, None, None) - split_vals = extra_show.split('|') - if len(split_vals) < 3: - return (extra_show, None, None) - show_dir = split_vals[0] - tvdb_id = split_vals[1] - show_name = '|'.join(split_vals[2:]) - - return (show_dir, tvdb_id, show_name) - - @cherrypy.expose - def addExistingShows(self, shows_to_add=None, promptForSettings=None): - """ - Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards - along to the newShow page. - """ - - # grab a list of other shows to add, if provided - if not shows_to_add: - shows_to_add = [] - elif type(shows_to_add) != list: - shows_to_add = [shows_to_add] - - shows_to_add = [urllib.unquote_plus(x) for x in shows_to_add] - - if promptForSettings == "on": - promptForSettings = 1 - else: - promptForSettings = 0 - - tvdb_id_given = [] - dirs_only = [] - # separate all the ones with TVDB IDs - for cur_dir in shows_to_add: - if not '|' in cur_dir: - dirs_only.append(cur_dir) - else: - show_dir, tvdb_id, show_name = self.split_extra_show(cur_dir) - if not show_dir or not tvdb_id or not show_name: - continue - tvdb_id_given.append((show_dir, int(tvdb_id), show_name)) - - - # if they want me to prompt for settings then I will just carry on to the newShow page - if promptForSettings and shows_to_add: - return self.newShow(shows_to_add[0], shows_to_add[1:]) - - # if they don't want me to prompt for settings then I can just add all the nfo shows now - num_added = 0 - for cur_show in tvdb_id_given: - show_dir, tvdb_id, show_name = cur_show - - # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.FLATTEN_FOLDERS_DEFAULT) #@UndefinedVariable - num_added += 1 - - if num_added: - ui.notifications.message("Shows Added", "Automatically added "+str(num_added)+" from their existing metadata files") - - # if we're done then go home - if not dirs_only: - redirect('/home') - - # for the remaining shows we need to prompt for each one, so forward this on to the newShow page - return self.newShow(dirs_only[0], dirs_only[1:]) - - - - -ErrorLogsMenu = [ - { 'title': 'Clear Errors', 'path': 'errorlogs/clearerrors' }, - #{ 'title': 'View Log', 'path': 'errorlogs/viewlog' }, -] - - -class ErrorLogs: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="errorlogs.tmpl") - t.submenu = ErrorLogsMenu - - return _munge(t) - - - @cherrypy.expose - def clearerrors(self): - classes.ErrorViewer.clear() - redirect("/errorlogs") - - @cherrypy.expose - def viewlog(self, minLevel=logger.MESSAGE, maxLines=500): - - t = PageTemplate(file="viewlogs.tmpl") - t.submenu = ErrorLogsMenu - - minLevel = int(minLevel) - - data = [] - if os.path.isfile(logger.sb_log_instance.log_file): - f = ek.ek(open, logger.sb_log_instance.log_file) - data = f.readlines() - f.close() - - regex = "^(\w{3})\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$" - - finalData = [] - - numLines = 0 - lastLine = False - numToShow = min(maxLines, len(data)) - - for x in reversed(data): - - x = x.decode('utf-8') - match = re.match(regex, x) - - if match: - level = match.group(6) - if level not in logger.reverseNames: - lastLine = False - continue - - if logger.reverseNames[level] >= minLevel: - lastLine = True - finalData.append(x) - else: - lastLine = False - continue - - elif lastLine: - finalData.append("AA"+x) - - numLines += 1 - - if numLines >= numToShow: - break - - result = "".join(finalData) - - t.logLines = result - t.minLevel = minLevel - - return _munge(t) - - -class Home: - - @cherrypy.expose - def is_alive(self, *args, **kwargs): - if 'callback' in kwargs and '_' in kwargs: - callback, _ = kwargs['callback'], kwargs['_'] - else: - return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query stiring." - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - cherrypy.response.headers['Content-Type'] = 'text/javascript' - cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' - cherrypy.response.headers['Access-Control-Allow-Headers'] = 'x-requested-with' - - if sickbeard.started: - return callback+'('+json.dumps({"msg": str(sickbeard.PID)})+');' - else: - return callback+'('+json.dumps({"msg": "nope"})+');' - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="home.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - addShows = NewHomeAddShows() - - postprocess = HomePostProcess() - - @cherrypy.expose - def testSABnzbd(self, host=None, username=None, password=None, apikey=None): - if not host.endswith("/"): - host = host + "/" - connection, accesMsg = sab.getSabAccesMethod(host, username, password, apikey) - if connection: - authed, authMsg = sab.testAuthentication(host, username, password, apikey) #@UnusedVariable - if authed: - return "Success. Connected and authenticated" - else: - return "Authentication failed. SABnzbd expects '"+accesMsg+"' as authentication method" - else: - return "Unable to connect to host" - - @cherrypy.expose - def testGrowl(self, host=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.growl_notifier.test_notify(host, password) - if password==None or password=='': - pw_append = '' - else: - pw_append = " with password: " + password - - if result: - return "Registered and Tested growl successfully "+urllib.unquote_plus(host)+pw_append - else: - return "Registration and Testing of growl failed "+urllib.unquote_plus(host)+pw_append - - @cherrypy.expose - def testProwl(self, prowl_api=None, prowl_priority=0): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) - if result: - return "Test prowl notice sent successfully" - else: - return "Test prowl notice failed" - - @cherrypy.expose - def testNotifo(self, username=None, apisecret=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.notifo_notifier.test_notify(username, apisecret) - if result: - return "Notifo notification succeeded. Check your Notifo clients to make sure it worked" - else: - return "Error sending Notifo notification" - - @cherrypy.expose - def testBoxcar(self, username=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.boxcar_notifier.test_notify(username) - if result: - return "Boxcar notification succeeded. Check your Boxcar clients to make sure it worked" - else: - return "Error sending Boxcar notification" - - @cherrypy.expose - def testPushover(self, userKey=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.pushover_notifier.test_notify(userKey) - if result: - return "Pushover notification succeeded. Check your Pushover clients to make sure it worked" - else: - return "Error sending Pushover notification" - - @cherrypy.expose - def twitterStep1(self): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - return notifiers.twitter_notifier._get_authorization() - - @cherrypy.expose - def twitterStep2(self, key): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.twitter_notifier._get_credentials(key) - logger.log(u"result: "+str(result)) - if result: - return "Key verification successful" - else: - return "Unable to verify key" - - @cherrypy.expose - def testTwitter(self): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.twitter_notifier.test_notify() - if result: - return "Tweet successful, check your twitter to make sure it worked" - else: - return "Error sending tweet" - - @cherrypy.expose - def testXBMC(self, host=None, username=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - finalResult = '' - for curHost in [x.strip() for x in host.split(",")]: - curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: - finalResult += "Test XBMC notice sent successfully to " + urllib.unquote_plus(curHost) - else: - finalResult += "Test XBMC notice failed to " + urllib.unquote_plus(curHost) - finalResult += "<br />\n" - - return finalResult - - @cherrypy.expose - def testPLEX(self, host=None, username=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - finalResult = '' - for curHost in [x.strip() for x in host.split(",")]: - curResult = notifiers.plex_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: - finalResult += "Test Plex notice sent successfully to " + urllib.unquote_plus(curHost) - else: - finalResult += "Test Plex notice failed to " + urllib.unquote_plus(curHost) - finalResult += "<br />\n" - - return finalResult - - @cherrypy.expose - def testLibnotify(self): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - if notifiers.libnotify_notifier.test_notify(): - return "Tried sending desktop notification via libnotify" - else: - return notifiers.libnotify.diagnose() - - @cherrypy.expose - def testNMJ(self, host=None, database=None, mount=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) - if result: - return "Successfull started the scan update" - else: - return "Test failed to start the scan update" - - @cherrypy.expose - def settingsNMJ(self, host=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) - if result: - return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % {"host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} - else: - return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' - - @cherrypy.expose - def testTrakt(self, api=None, username=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.trakt_notifier.test_notify(api, username, password) - if result: - return "Test notice sent successfully to Trakt" - else: - return "Test notice failed to Trakt" - - @cherrypy.expose - def testNMA(self, nma_api=None, nma_priority=0): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) - if result: - return "Test NMA notice sent successfully" - else: - return "Test NMA notice failed" - - @cherrypy.expose - def shutdown(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home") - - threading.Timer(2, sickbeard.invoke_shutdown).start() - - title = "Shutting down" - message = "Sick Beard is shutting down..." - - return _genericMessage(title, message) - - @cherrypy.expose - def restart(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home") - - t = PageTemplate(file="restart.tmpl") - t.submenu = HomeMenu() - - # do a soft restart - threading.Timer(2, sickbeard.invoke_restart, [False]).start() - - return _munge(t) - - @cherrypy.expose - def update(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home") - - updated = sickbeard.versionCheckScheduler.action.update() #@UndefinedVariable - - if updated: - # do a hard restart - threading.Timer(2, sickbeard.invoke_restart, [False]).start() - t = PageTemplate(file="restart_bare.tmpl") - return _munge(t) - else: - return _genericMessage("Update Failed","Update wasn't successful, not restarting. Check your log for more information.") - - @cherrypy.expose - def displayShow(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - else: - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Show not in show list") - - myDB = db.DBConnection() - - seasonResults = myDB.select( - "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season desc", - [showObj.tvdbid] - ) - - sqlResults = myDB.select( - "SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", - [showObj.tvdbid] - ) - - t = PageTemplate(file="displayShow.tmpl") - t.submenu = [ { 'title': 'Edit', 'path': 'home/editShow?show=%d'%showObj.tvdbid } ] - - try: - t.showLoc = (showObj.location, True) - except sickbeard.exceptions.ShowDirNotFoundException: - t.showLoc = (showObj._location, False) - - show_message = '' - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable - show_message = 'This show is in the process of being downloaded from theTVDB.com - the info below is incomplete.' - - elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable - show_message = 'The information below is in the process of being updated.' - - elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): #@UndefinedVariable - show_message = 'The episodes below are currently being refreshed from disk' - - elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): #@UndefinedVariable - show_message = 'This show is queued to be refreshed.' - - elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): #@UndefinedVariable - show_message = 'This show is queued and awaiting an update.' - - if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable - if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable - t.submenu.append({ 'title': 'Delete', 'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True }) - t.submenu.append({ 'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d'%showObj.tvdbid }) - t.submenu.append({ 'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1'%showObj.tvdbid }) - t.submenu.append({ 'title': 'Update show in XBMC', 'path': 'home/updateXBMC?showName=%s'%urllib.quote_plus(showObj.name.encode('utf-8')), 'requires': haveXBMC }) - t.submenu.append({ 'title': 'Preview Rename', 'path': 'home/testRename?show=%d'%showObj.tvdbid }) - - t.show = showObj - t.sqlResults = sqlResults - t.seasonResults = seasonResults - t.show_message = show_message - - epCounts = {} - epCats = {} - epCounts[Overview.SKIPPED] = 0 - epCounts[Overview.WANTED] = 0 - epCounts[Overview.QUAL] = 0 - epCounts[Overview.GOOD] = 0 - epCounts[Overview.UNAIRED] = 0 - - for curResult in sqlResults: - - curEpCat = showObj.getOverview(int(curResult["status"])) - epCats[str(curResult["season"])+"x"+str(curResult["episode"])] = curEpCat - epCounts[curEpCat] += 1 - - def titler(x): - if not x: - return x - if x.lower().startswith('a '): - x = x[2:] - elif x.lower().startswith('the '): - x = x[4:] - return x - t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) - - t.epCounts = epCounts - t.epCats = epCats - - return _munge(t) - - @cherrypy.expose - def plotDetails(self, show, season, episode): - result = db.DBConnection().action("SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", (show, season, episode)).fetchone() - return result['description'] if result else 'Episode not found.' - - @cherrypy.expose - def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], flatten_folders=None, paused=None, directCall=False, air_by_date=None, tvdbLang=None, audio_lang=None, custom_search_names=None): - - if show == None: - errString = "Invalid show ID: "+str(show) - if directCall: - return [errString] - else: - return _genericMessage("Error", errString) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - errString = "Unable to find the specified show: "+str(show) - if directCall: - return [errString] - else: - return _genericMessage("Error", errString) - - if not location and not anyQualities and not bestQualities and not flatten_folders: - - t = PageTemplate(file="editShow.tmpl") - t.submenu = HomeMenu() - with showObj.lock: - t.show = showObj - - return _munge(t) - - if flatten_folders == "on": - flatten_folders = 1 - else: - flatten_folders = 0 - - logger.log(u"flatten folders: "+str(flatten_folders)) - - if paused == "on": - paused = 1 - else: - paused = 0 - - if air_by_date == "on": - air_by_date = 1 - else: - air_by_date = 0 - - if tvdbLang and tvdbLang in tvdb_api.Tvdb().config['valid_languages']: - tvdb_lang = tvdbLang - else: - tvdb_lang = showObj.lang - - # if we changed the language then kick off an update - if tvdb_lang == showObj.lang: - do_update = False - else: - do_update = True - - if type(anyQualities) != list: - anyQualities = [anyQualities] - - if type(bestQualities) != list: - bestQualities = [bestQualities] - - errors = [] - with showObj.lock: - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - showObj.quality = newQuality - - # reversed for now - if bool(showObj.flatten_folders) != bool(flatten_folders): - showObj.flatten_folders = flatten_folders - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show: "+ex(e)) - - showObj.paused = paused - showObj.air_by_date = air_by_date - showObj.lang = tvdb_lang - showObj.audio_lang = audio_lang - showObj.custom_search_names = custom_search_names - - # if we change location clear the db of episodes, change it, write to db, and rescan - if os.path.normpath(showObj._location) != os.path.normpath(location): - logger.log(os.path.normpath(showObj._location)+" != "+os.path.normpath(location), logger.DEBUG) - if not ek.ek(os.path.isdir, location): - errors.append("New location <tt>%s</tt> does not exist" % location) - - # don't bother if we're going to update anyway - elif not do_update: - # change it - try: - showObj.location = location - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show:"+ex(e)) - # grab updated info from TVDB - #showObj.loadEpisodesFromTVDB() - # rescan the episodes in the new folder - except exceptions.NoNFOException: - errors.append("The folder at <tt>%s</tt> doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in Sick Beard." % location) - - # save it to the DB - showObj.saveToDB() - - # force the update - if do_update: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable - time.sleep(1) - except exceptions.CantUpdateException, e: - errors.append("Unable to force an update on the show.") - - if directCall: - return errors - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - '<ul>' + '\n'.join(['<li>%s</li>' % error for error in errors]) + "</ul>") - - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def deleteShow(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable - return _genericMessage("Error", "Shows can't be deleted while they're being added or updated.") - - showObj.deleteShow() - - ui.notifications.message('<b>%s</b> has been deleted' % showObj.name) - redirect("/home") - - @cherrypy.expose - def refreshShow(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - # force the update from the DB - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - except exceptions.CantRefreshException, e: - ui.notifications.error("Unable to refresh this show.", - ex(e)) - - time.sleep(3) - - redirect("/home/displayShow?show="+str(showObj.tvdbid)) - - @cherrypy.expose - def updateShow(self, show=None, force=0): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - # force the update - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) #@UndefinedVariable - except exceptions.CantUpdateException, e: - ui.notifications.error("Unable to update this show.", - ex(e)) - - # just give it some time - time.sleep(3) - - redirect("/home/displayShow?show="+str(showObj.tvdbid)) - - @cherrypy.expose - def updateXBMC(self, showName=None): - # TODO: configure that each host can have different options / username / pw - # only send update to first host in the list -- workaround for xbmc sql backend users - firstHost = sickbeard.XBMC_HOST.split(",")[0].strip() - if notifiers.xbmc_notifier.update_library(showName=showName): - ui.notifications.message("Library update command sent to XBMC host: " + firstHost) - else: - ui.notifications.error("Unable to contact XBMC host: " + firstHost) - redirect('/home') - - @cherrypy.expose - def updatePLEX(self): - if notifiers.plex_notifier.update_library(): - ui.notifications.message("Library update command sent to Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) - else: - ui.notifications.error("Unable to contact Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) - redirect('/home') - - @cherrypy.expose - def setStatus(self, show=None, eps=None, status=None, direct=False): - - if show == None or eps == None or status == None: - errMsg = "You must specify a show and at least one episode" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - if not statusStrings.has_key(int(status)): - errMsg = "Invalid status" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - errMsg = "Error", "Show not in show list" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - segment_list = [] - - if eps != None: - - for curEp in eps.split('|'): - - logger.log(u"Attempting to set status on episode "+curEp+" to "+status, logger.DEBUG) - - epInfo = curEp.split('x') - - epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) - - if int(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) - - if epObj == None: - return _genericMessage("Error", "Episode couldn't be retrieved") - - with epObj.lock: - # don't let them mess up UNAIRED episodes - if epObj.status == UNAIRED: - logger.log(u"Refusing to change status of "+curEp+" because it is UNAIRED", logger.ERROR) - continue - - if int(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): - logger.log(u"Refusing to change status of "+curEp+" to DOWNLOADED because it's not SNATCHED/DOWNLOADED", logger.ERROR) - continue - - epObj.status = int(status) - epObj.saveToDB() - - msg = "Backlog was automatically started for the following seasons of <b>"+showObj.name+"</b>:<br />" - for cur_segment in segment_list: - msg += "<li>Season "+str(cur_segment)+"</li>" - logger.log(u"Sending backlog for "+showObj.name+" season "+str(cur_segment)+" because some eps were set to wanted") - cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, cur_segment) - sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable - msg += "</ul>" - - if segment_list: - ui.notifications.message("Backlog started", msg) - - if direct: - return json.dumps({'result': 'success'}) - else: - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def testRename(self, show=None): - - if show == None: - return _genericMessage("Error", "You must specify a show") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Show not in show list") - - try: - show_loc = showObj.location #@UnusedVariable - except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - ep_obj_rename_list = [] - - ep_obj_list = showObj.getAllEpisodes(has_location=True) - - for cur_ep_obj in ep_obj_list: - # Only want to rename if we have a location - if cur_ep_obj.location: - if cur_ep_obj.relatedEps: - # do we have one of multi-episodes in the rename list already - have_already = False - for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: - if cur_related_ep in ep_obj_rename_list: - have_already = True - break - if not have_already: - ep_obj_rename_list.append(cur_ep_obj) - - else: - ep_obj_rename_list.append(cur_ep_obj) - - if ep_obj_rename_list: - # present season DESC episode DESC on screen - ep_obj_rename_list.reverse() - - t = PageTemplate(file="testRename.tmpl") - t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.tvdbid}] - t.ep_obj_list = ep_obj_rename_list - t.show = showObj - - return _munge(t) - - @cherrypy.expose - def doRename(self, show=None, eps=None): - - if show == None or eps == None: - errMsg = "You must specify a show and at least one episode" - return _genericMessage("Error", errMsg) - - show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if show_obj == None: - errMsg = "Error", "Show not in show list" - return _genericMessage("Error", errMsg) - - try: - show_loc = show_obj.location #@UnusedVariable - except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - myDB = db.DBConnection() - - if eps == None: - redirect("/home/displayShow?show=" + show) - - for curEp in eps.split('|'): - - epInfo = curEp.split('x') - - # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database - ep_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", [show, epInfo[0], epInfo[1]]) - if not ep_result: - logger.log(u"Unable to find an episode for "+curEp+", skipping", logger.WARNING) - continue - related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE location = ? AND episode != ?", [ep_result[0]["location"], epInfo[1]]) - - root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) - for cur_related_ep in related_eps_result: - related_ep_obj = show_obj.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) - if related_ep_obj not in root_ep_obj.relatedEps: - root_ep_obj.relatedEps.append(related_ep_obj) - - root_ep_obj.rename() - - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def searchEpisode(self, show=None, season=None, episode=None): - - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj) - sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) #@UndefinedVariable - - # wait until the queue item tells us whether it worked or not - while ep_queue_item.success == None: #@UndefinedVariable - time.sleep(1) - - # return the correct json value - if ep_queue_item.success: - return json.dumps({'result': statusStrings[ep_obj.status]}) - - return json.dumps({'result': 'failure'}) - -class UI: - - @cherrypy.expose - def add_message(self): - - ui.notifications.message('Test 1', 'This is test number 1') - ui.notifications.error('Test 2', 'This is test number 2') - - return "ok" - - @cherrypy.expose - def get_messages(self): - messages = {} - cur_notification_num = 1 - for cur_notification in ui.notifications.get_notifications(): - messages['notification-'+str(cur_notification_num)] = {'title': cur_notification.title, - 'message': cur_notification.message, - 'type': cur_notification.type} - cur_notification_num += 1 - - return json.dumps(messages) - - -class WebInterface: - - @cherrypy.expose - def index(self): - - redirect("/home") - - @cherrypy.expose - def showPoster(self, show=None, which=None): - - if which == 'poster': - default_image_name = 'poster.png' - else: - default_image_name = 'banner.png' - - default_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', default_image_name) - if show is None: - return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") - else: - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") - - cache_obj = image_cache.ImageCache() - - if which == 'poster': - image_file_name = cache_obj.poster_path(showObj.tvdbid) - # this is for 'banner' but also the default case - else: - image_file_name = cache_obj.banner_path(showObj.tvdbid) - - if ek.ek(os.path.isfile, image_file_name): - # use startup argument to prevent using PIL even if installed - if sickbeard.NO_RESIZE: - return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") - try: - from PIL import Image - from cStringIO import StringIO - except ImportError: # PIL isn't installed - return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") - else: - im = Image.open(image_file_name) - if im.mode == 'P': # Convert GIFs to RGB - im = im.convert('RGB') - if which == 'banner': - size = 606, 112 - elif which == 'poster': - size = 136, 200 - else: - return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") - im = im.resize(size, Image.ANTIALIAS) - imgbuffer = StringIO() - im.save(imgbuffer, 'JPEG', quality=85) - cherrypy.response.headers['Content-Type'] = 'image/jpeg' - return imgbuffer.getvalue() - else: - return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") - - @cherrypy.expose - def setComingEpsLayout(self, layout): - if layout not in ('poster', 'banner', 'list'): - layout = 'banner' - - sickbeard.COMING_EPS_LAYOUT = layout - - redirect("/comingEpisodes") - - @cherrypy.expose - def toggleComingEpsDisplayPaused(self): - - sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED - - redirect("/comingEpisodes") - - @cherrypy.expose - def setComingEpsSort(self, sort): - if sort not in ('date', 'network', 'show'): - sort = 'date' - - sickbeard.COMING_EPS_SORT = sort - - redirect("/comingEpisodes") - - @cherrypy.expose - def comingEpisodes(self, layout="None"): - - myDB = db.DBConnection() - - today = datetime.date.today().toordinal() - next_week = (datetime.date.today() + datetime.timedelta(days=7)).toordinal() - recently = (datetime.date.today() - datetime.timedelta(days=3)).toordinal() - - done_show_list = [] - qualList = Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED, IGNORED] - sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND airdate >= ? AND airdate < ? AND tv_shows.tvdb_id = tv_episodes.showid AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, next_week] + qualList) - for cur_result in sql_results: - done_show_list.append(int(cur_result["showid"])) - - more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes outer_eps, tv_shows WHERE season != 0 AND showid NOT IN ("+','.join(['?']*len(done_show_list))+") AND tv_shows.tvdb_id = outer_eps.showid AND airdate = (SELECT airdate FROM tv_episodes inner_eps WHERE 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) - sql_results += more_sql_results - - more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND tv_shows.tvdb_id = tv_episodes.showid AND airdate < ? AND airdate >= ? AND tv_episodes.status = ? AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, recently, WANTED] + qualList) - sql_results += more_sql_results - - #epList = sickbeard.comingList - - # sort by air date - sorts = { - 'date': (lambda x, y: cmp(int(x["airdate"]), int(y["airdate"]))), - 'show': (lambda a, b: cmp(a["show_name"], b["show_name"])), - 'network': (lambda a, b: cmp(a["network"], b["network"])), - } - - #epList.sort(sorts[sort]) - sql_results.sort(sorts[sickbeard.COMING_EPS_SORT]) - - t = PageTemplate(file="comingEpisodes.tmpl") - paused_item = { 'title': '', 'path': 'toggleComingEpsDisplayPaused' } - paused_item['title'] = 'Hide Paused' if sickbeard.COMING_EPS_DISPLAY_PAUSED else 'Show Paused' - t.submenu = [ - { 'title': 'Sort by:', 'path': {'Date': 'setComingEpsSort/?sort=date', - 'Show': 'setComingEpsSort/?sort=show', - 'Network': 'setComingEpsSort/?sort=network', - }}, - - { 'title': 'Layout:', 'path': {'Banner': 'setComingEpsLayout/?layout=banner', - 'Poster': 'setComingEpsLayout/?layout=poster', - 'List': 'setComingEpsLayout/?layout=list', - }}, - paused_item, - ] - - t.next_week = next_week - t.today = today - t.sql_results = sql_results - - # Allow local overriding of layout parameter - if layout and layout in ('poster', 'banner', 'list'): - t.layout = layout - else: - t.layout = sickbeard.COMING_EPS_LAYOUT - - - return _munge(t) - - manage = Manage() - - history = History() - - config = Config() - - home = Home() - - api = Api() - - browser = browser.WebFileBrowser() - - errorlogs = ErrorLogs() - - ui = UI() +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import os.path + +import time +import urllib +import re +import threading +import datetime +import random + +from Cheetah.Template import Template +import cherrypy.lib + +import sickbeard + +from sickbeard import config, sab +from sickbeard import history, notifiers, processTV +from sickbeard import tv, ui +from sickbeard import logger, helpers, exceptions, classes, db +from sickbeard import encodingKludge as ek +from sickbeard import search_queue +from sickbeard import image_cache +from sickbeard import naming + +from sickbeard.providers import newznab +from sickbeard.common import Quality, Overview, statusStrings, showLanguages, audio +from sickbeard.common import SNATCHED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED +from sickbeard.exceptions import ex +from sickbeard.webapi import Api + +from lib.tvdb_api import tvdb_api + +try: + import json +except ImportError: + from lib import simplejson as json + +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from sickbeard import browser + + +class PageTemplate (Template): + def __init__(self, *args, **KWs): + KWs['file'] = os.path.join(sickbeard.PROG_DIR, "data/interfaces/default/", KWs['file']) + super(PageTemplate, self).__init__(*args, **KWs) + self.sbRoot = sickbeard.WEB_ROOT + self.sbHttpPort = sickbeard.WEB_PORT + self.sbHttpsPort = sickbeard.WEB_PORT + self.sbHttpsEnabled = sickbeard.ENABLE_HTTPS + if cherrypy.request.headers['Host'][0] == '[': + self.sbHost = re.match("^\[.*\]", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) + else: + self.sbHost = re.match("^[^:]+", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) + self.projectHomePage = "http://code.google.com/p/sickbeard/" + + if sickbeard.NZBS and sickbeard.NZBS_UID and sickbeard.NZBS_HASH: + logger.log(u"NZBs.org has been replaced, please check the config to configure the new provider!", logger.ERROR) + ui.notifications.error("NZBs.org Config Update", "NZBs.org has a new site. Please <a href=\""+sickbeard.WEB_ROOT+"/config/providers\">update your config</a> with the api key from <a href=\"http://beta.nzbs.org/login\">http://beta.nzbs.org</a> and then disable the old NZBs.org provider.") + + if "X-Forwarded-Host" in cherrypy.request.headers: + self.sbHost = cherrypy.request.headers['X-Forwarded-Host'] + if "X-Forwarded-Port" in cherrypy.request.headers: + self.sbHttpPort = cherrypy.request.headers['X-Forwarded-Port'] + self.sbHttpsPort = self.sbHttpPort + if "X-Forwarded-Proto" in cherrypy.request.headers: + self.sbHttpsEnabled = True if cherrypy.request.headers['X-Forwarded-Proto'] == 'https' else False + + logPageTitle = 'Logs & Errors' + if len(classes.ErrorViewer.errors): + logPageTitle += ' ('+str(len(classes.ErrorViewer.errors))+')' + self.logPageTitle = logPageTitle + self.sbPID = str(sickbeard.PID) + self.menu = [ + { 'title': 'Home', 'key': 'home' }, + { 'title': 'Coming Episodes', 'key': 'comingEpisodes' }, + { 'title': 'History', 'key': 'history' }, + { 'title': 'Manage', 'key': 'manage' }, + { 'title': 'Config', 'key': 'config' }, + { 'title': logPageTitle, 'key': 'errorlogs' }, + ] + +def redirect(abspath, *args, **KWs): + assert abspath[0] == '/' + raise cherrypy.HTTPRedirect(sickbeard.WEB_ROOT + abspath, *args, **KWs) + +class TVDBWebUI: + def __init__(self, config, log=None): + self.config = config + self.log = log + + def selectSeries(self, allSeries): + + searchList = ",".join([x['id'] for x in allSeries]) + showDirList = "" + for curShowDir in self.config['_showDir']: + showDirList += "showDir="+curShowDir+"&" + redirect("/home/addShows/addShow?" + showDirList + "seriesList=" + searchList) + +def _munge(string): + return unicode(string).encode('utf-8', 'xmlcharrefreplace') + +def _genericMessage(subject, message): + t = PageTemplate(file="genericMessage.tmpl") + t.submenu = HomeMenu() + t.subject = subject + t.message = message + return _munge(t) + +def _getEpisode(show, season, episode): + + if show == None or season == None or episode == None: + return "Invalid parameters" + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return "Show not in show list" + + epObj = showObj.getEpisode(int(season), int(episode)) + + if epObj == None: + return "Episode couldn't be retrieved" + + return epObj + +ManageMenu = [ + { 'title': 'Backlog Overview', 'path': 'manage/backlogOverview' }, + { 'title': 'Manage Searches', 'path': 'manage/manageSearches' }, + { 'title': 'Episode Status Management', 'path': 'manage/episodeStatuses' }, +] + +class ManageSearches: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="manage_manageSearches.tmpl") + #t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() + t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() #@UndefinedVariable + t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() #@UndefinedVariable + t.searchStatus = sickbeard.currentSearchScheduler.action.amActive #@UndefinedVariable + t.submenu = ManageMenu + + return _munge(t) + + @cherrypy.expose + def forceSearch(self): + + # force it to run the next time it looks + result = sickbeard.currentSearchScheduler.forceRun() + if result: + logger.log(u"Search forced") + ui.notifications.message('Episode search started', + 'Note: RSS feeds may not be updated if retrieved recently') + + redirect("/manage/manageSearches") + + @cherrypy.expose + def pauseBacklog(self, paused=None): + if paused == "1": + sickbeard.searchQueueScheduler.action.pause_backlog() #@UndefinedVariable + else: + sickbeard.searchQueueScheduler.action.unpause_backlog() #@UndefinedVariable + + redirect("/manage/manageSearches") + + @cherrypy.expose + def forceVersionCheck(self): + + # force a check to see if there is a new version + result = sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) #@UndefinedVariable + if result: + logger.log(u"Forcing version check") + + redirect("/manage/manageSearches") + + +class Manage: + + manageSearches = ManageSearches() + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="manage.tmpl") + t.submenu = ManageMenu + return _munge(t) + + @cherrypy.expose + def showEpisodeStatuses(self, tvdb_id, whichStatus): + myDB = db.DBConnection() + + status_list = [int(whichStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + + cur_show_results = myDB.select("SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN ("+','.join(['?']*len(status_list))+")", [int(tvdb_id)] + status_list) + + result = {} + for cur_result in cur_show_results: + cur_season = int(cur_result["season"]) + cur_episode = int(cur_result["episode"]) + + if cur_season not in result: + result[cur_season] = {} + + result[cur_season][cur_episode] = cur_result["name"] + + return json.dumps(result) + + @cherrypy.expose + def episodeStatuses(self, whichStatus=None): + + if whichStatus: + whichStatus = int(whichStatus) + status_list = [whichStatus] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + else: + status_list = [] + + t = PageTemplate(file="manage_episodeStatuses.tmpl") + t.submenu = ManageMenu + t.whichStatus = whichStatus + + # if we have no status then this is as far as we need to go + if not status_list: + return _munge(t) + + myDB = db.DBConnection() + status_results = myDB.select("SELECT show_name, tv_shows.tvdb_id as tvdb_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND tv_episodes.showid = tv_shows.tvdb_id ORDER BY show_name", status_list) + + ep_counts = {} + show_names = {} + sorted_show_ids = [] + for cur_status_result in status_results: + cur_tvdb_id = int(cur_status_result["tvdb_id"]) + if cur_tvdb_id not in ep_counts: + ep_counts[cur_tvdb_id] = 1 + else: + ep_counts[cur_tvdb_id] += 1 + + show_names[cur_tvdb_id] = cur_status_result["show_name"] + if cur_tvdb_id not in sorted_show_ids: + sorted_show_ids.append(cur_tvdb_id) + + t.show_names = show_names + t.ep_counts = ep_counts + t.sorted_show_ids = sorted_show_ids + return _munge(t) + + @cherrypy.expose + def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): + + status_list = [int(oldStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + + to_change = {} + + # make a list of all shows and their associated args + for arg in kwargs: + tvdb_id, what = arg.split('-') + + # we don't care about unchecked checkboxes + if kwargs[arg] != 'on': + continue + + if tvdb_id not in to_change: + to_change[tvdb_id] = [] + + to_change[tvdb_id].append(what) + + myDB = db.DBConnection() + + for cur_tvdb_id in to_change: + + # get a list of all the eps we want to change if they just said "all" + if 'all' in to_change[cur_tvdb_id]: + all_eps_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND showid = ?", status_list + [cur_tvdb_id]) + all_eps = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] + to_change[cur_tvdb_id] = all_eps + + Home().setStatus(cur_tvdb_id, '|'.join(to_change[cur_tvdb_id]), newStatus, direct=True) + + redirect('/manage/episodeStatuses') + + @cherrypy.expose + def backlogShow(self, tvdb_id): + + show_obj = helpers.findCertainShow(sickbeard.showList, int(tvdb_id)) + + if show_obj: + sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) #@UndefinedVariable + + redirect("/manage/backlogOverview") + + @cherrypy.expose + def backlogOverview(self): + + t = PageTemplate(file="manage_backlogOverview.tmpl") + t.submenu = ManageMenu + + myDB = db.DBConnection() + + showCounts = {} + showCats = {} + showSQLResults = {} + + for curShow in sickbeard.showList: + + epCounts = {} + epCats = {} + epCounts[Overview.SKIPPED] = 0 + epCounts[Overview.WANTED] = 0 + epCounts[Overview.QUAL] = 0 + epCounts[Overview.GOOD] = 0 + epCounts[Overview.UNAIRED] = 0 + + sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", [curShow.tvdbid]) + + for curResult in sqlResults: + + curEpCat = curShow.getOverview(int(curResult["status"])) + epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat + epCounts[curEpCat] += 1 + + showCounts[curShow.tvdbid] = epCounts + showCats[curShow.tvdbid] = epCats + showSQLResults[curShow.tvdbid] = sqlResults + + t.showCounts = showCounts + t.showCats = showCats + t.showSQLResults = showSQLResults + + return _munge(t) + + @cherrypy.expose + def massEdit(self, toEdit=None): + + t = PageTemplate(file="manage_massEdit.tmpl") + t.submenu = ManageMenu + + if not toEdit: + redirect("/manage") + + showIDs = toEdit.split("|") + showList = [] + for curID in showIDs: + curID = int(curID) + showObj = helpers.findCertainShow(sickbeard.showList, curID) + if showObj: + showList.append(showObj) + + flatten_folders_all_same = True + last_flatten_folders = None + + paused_all_same = True + last_paused = None + + quality_all_same = True + last_quality = None + + root_dir_list = [] + + for curShow in showList: + + cur_root_dir = ek.ek(os.path.dirname, curShow._location) + if cur_root_dir not in root_dir_list: + root_dir_list.append(cur_root_dir) + + # if we know they're not all the same then no point even bothering + if paused_all_same: + # if we had a value already and this value is different then they're not all the same + if last_paused not in (curShow.paused, None): + paused_all_same = False + else: + last_paused = curShow.paused + + if flatten_folders_all_same: + if last_flatten_folders not in (None, curShow.flatten_folders): + flatten_folders_all_same = False + else: + last_flatten_folders = curShow.flatten_folders + + if quality_all_same: + if last_quality not in (None, curShow.quality): + quality_all_same = False + else: + last_quality = curShow.quality + + t.showList = toEdit + t.paused_value = last_paused if paused_all_same else None + t.flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None + t.quality_value = last_quality if quality_all_same else None + t.root_dir_list = root_dir_list + + return _munge(t) + + @cherrypy.expose + def massEditSubmit(self, paused=None, flatten_folders=None, quality_preset=False, + anyQualities=[], bestQualities=[], toEdit=None, *args, **kwargs): + + dir_map = {} + for cur_arg in kwargs: + if not cur_arg.startswith('orig_root_dir_'): + continue + which_index = cur_arg.replace('orig_root_dir_', '') + end_dir = kwargs['new_root_dir_'+which_index] + dir_map[kwargs[cur_arg]] = end_dir + + showIDs = toEdit.split("|") + errors = [] + for curShow in showIDs: + curErrors = [] + showObj = helpers.findCertainShow(sickbeard.showList, int(curShow)) + if not showObj: + continue + + cur_root_dir = ek.ek(os.path.dirname, showObj._location) + cur_show_dir = ek.ek(os.path.basename, showObj._location) + if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: + new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) + logger.log(u"For show "+showObj.name+" changing dir from "+showObj._location+" to "+new_show_dir) + else: + new_show_dir = showObj._location + + if paused == 'keep': + new_paused = showObj.paused + else: + new_paused = True if paused == 'enable' else False + new_paused = 'on' if new_paused else 'off' + + if flatten_folders == 'keep': + new_flatten_folders = showObj.flatten_folders + else: + new_flatten_folders = True if flatten_folders == 'enable' else False + new_flatten_folders = 'on' if new_flatten_folders else 'off' + + if quality_preset == 'keep': + anyQualities, bestQualities = Quality.splitQuality(showObj.quality) + + curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, new_flatten_folders, new_paused, directCall=True) + + if curErrors: + logger.log(u"Errors: "+str(curErrors), logger.ERROR) + errors.append('<b>%s:</b>\n<ul>' % showObj.name + ' '.join(['<li>%s</li>' % error for error in curErrors]) + "</ul>") + + if len(errors) > 0: + ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), + " ".join(errors)) + + redirect("/manage") + + @cherrypy.expose + def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toMetadata=None): + + if toUpdate != None: + toUpdate = toUpdate.split('|') + else: + toUpdate = [] + + if toRefresh != None: + toRefresh = toRefresh.split('|') + else: + toRefresh = [] + + if toRename != None: + toRename = toRename.split('|') + else: + toRename = [] + + if toDelete != None: + toDelete = toDelete.split('|') + else: + toDelete = [] + + if toMetadata != None: + toMetadata = toMetadata.split('|') + else: + toMetadata = [] + + errors = [] + refreshes = [] + updates = [] + renames = [] + + for curShowID in set(toUpdate+toRefresh+toRename+toDelete+toMetadata): + + if curShowID == '': + continue + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(curShowID)) + + if showObj == None: + continue + + if curShowID in toDelete: + showObj.deleteShow() + # don't do anything else if it's being deleted + continue + + if curShowID in toUpdate: + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable + updates.append(showObj.name) + except exceptions.CantUpdateException, e: + errors.append("Unable to update show "+showObj.name+": "+ex(e)) + + # don't bother refreshing shows that were updated anyway + if curShowID in toRefresh and curShowID not in toUpdate: + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + refreshes.append(showObj.name) + except exceptions.CantRefreshException, e: + errors.append("Unable to refresh show "+showObj.name+": "+ex(e)) + + if curShowID in toRename: + sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) #@UndefinedVariable + renames.append(showObj.name) + + if len(errors) > 0: + ui.notifications.error("Errors encountered", + '<br >\n'.join(errors)) + + messageDetail = "" + + if len(updates) > 0: + messageDetail += "<br /><b>Updates</b><br /><ul><li>" + messageDetail += "</li><li>".join(updates) + messageDetail += "</li></ul>" + + if len(refreshes) > 0: + messageDetail += "<br /><b>Refreshes</b><br /><ul><li>" + messageDetail += "</li><li>".join(refreshes) + messageDetail += "</li></ul>" + + if len(renames) > 0: + messageDetail += "<br /><b>Renames</b><br /><ul><li>" + messageDetail += "</li><li>".join(renames) + messageDetail += "</li></ul>" + + if len(updates+refreshes+renames) > 0: + ui.notifications.message("The following actions were queued:", + messageDetail) + + redirect("/manage") + + +class History: + + @cherrypy.expose + def index(self, limit=100): + + myDB = db.DBConnection() + +# sqlResults = myDB.select("SELECT h.*, show_name, name FROM history h, tv_shows s, tv_episodes e WHERE h.showid=s.tvdb_id AND h.showid=e.showid AND h.season=e.season AND h.episode=e.episode ORDER BY date DESC LIMIT "+str(numPerPage*(p-1))+", "+str(numPerPage)) + if limit == "0": + sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC") + else: + sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC LIMIT ?", [limit]) + + t = PageTemplate(file="history.tmpl") + t.historyResults = sqlResults + t.limit = limit + t.submenu = [ + { 'title': 'Clear History', 'path': 'history/clearHistory' }, + { 'title': 'Trim History', 'path': 'history/trimHistory' }, + ] + + return _munge(t) + + + @cherrypy.expose + def clearHistory(self): + + myDB = db.DBConnection() + myDB.action("DELETE FROM history WHERE 1=1") + ui.notifications.message('History cleared') + redirect("/history") + + + @cherrypy.expose + def trimHistory(self): + + myDB = db.DBConnection() + myDB.action("DELETE FROM history WHERE date < "+str((datetime.datetime.today()-datetime.timedelta(days=30)).strftime(history.dateFormat))) + ui.notifications.message('Removed history entries greater than 30 days old') + redirect("/history") + + +ConfigMenu = [ + { 'title': 'General', 'path': 'config/general/' }, + { 'title': 'Search Settings', 'path': 'config/search/' }, + { 'title': 'Search Providers', 'path': 'config/providers/' }, + { 'title': 'Post Processing', 'path': 'config/postProcessing/' }, + { 'title': 'Notifications', 'path': 'config/notifications/' }, +] + +class ConfigGeneral: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config_general.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveRootDirs(self, rootDirString=None): + sickbeard.ROOT_DIRS = rootDirString + + @cherrypy.expose + def saveAddShowDefaults(self, defaultFlattenFolders, defaultStatus, anyQualities, bestQualities): + + if anyQualities: + anyQualities = anyQualities.split(',') + else: + anyQualities = [] + + if bestQualities: + bestQualities = bestQualities.split(',') + else: + bestQualities = [] + + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + + sickbeard.STATUS_DEFAULT = int(defaultStatus) + sickbeard.QUALITY_DEFAULT = int(newQuality) + + if defaultFlattenFolders == "true": + defaultFlattenFolders = 1 + else: + defaultFlattenFolders = 0 + + sickbeard.FLATTEN_FOLDERS_DEFAULT = int(defaultFlattenFolders) + + @cherrypy.expose + def generateKey(self): + """ Return a new randomized API_KEY + """ + + try: + from hashlib import md5 + except ImportError: + from md5 import md5 + + # Create some values to seed md5 + t = str(time.time()) + r = str(random.random()) + + # Create the md5 instance and give it the current time + m = md5(t) + + # Update the md5 instance with the random variable + m.update(r) + + # Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b + logger.log(u"New API generated") + return m.hexdigest() + + @cherrypy.expose + def saveGeneral(self, log_dir=None, web_port=None, web_log=None, web_ipv6=None, + launch_browser=None, web_username=None, use_api=None, api_key=None, + web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None): + + results = [] + + if web_ipv6 == "on": + web_ipv6 = 1 + else: + web_ipv6 = 0 + + if web_log == "on": + web_log = 1 + else: + web_log = 0 + + if launch_browser == "on": + launch_browser = 1 + else: + launch_browser = 0 + + if version_notify == "on": + version_notify = 1 + else: + version_notify = 0 + + if not config.change_LOG_DIR(log_dir): + results += ["Unable to create directory " + os.path.normpath(log_dir) + ", log dir not changed."] + + sickbeard.LAUNCH_BROWSER = launch_browser + + sickbeard.WEB_PORT = int(web_port) + sickbeard.WEB_IPV6 = web_ipv6 + sickbeard.WEB_LOG = web_log + sickbeard.WEB_USERNAME = web_username + sickbeard.WEB_PASSWORD = web_password + + if use_api == "on": + use_api = 1 + else: + use_api = 0 + + sickbeard.USE_API = use_api + sickbeard.API_KEY = api_key + + if enable_https == "on": + enable_https = 1 + else: + enable_https = 0 + + sickbeard.ENABLE_HTTPS = enable_https + + if not config.change_HTTPS_CERT(https_cert): + results += ["Unable to create directory " + os.path.normpath(https_cert) + ", https cert dir not changed."] + + if not config.change_HTTPS_KEY(https_key): + results += ["Unable to create directory " + os.path.normpath(https_key) + ", https key dir not changed."] + + config.change_VERSION_NOTIFY(version_notify) + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/general/") + + +class ConfigSearch: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config_search.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, + sab_apikey=None, sab_category=None, sab_host=None, nzbget_password=None, nzbget_category=None, nzbget_host=None, + torrent_dir=None, nzb_method=None, usenet_retention=None, search_frequency=None, download_propers=None): + + results = [] + + if not config.change_NZB_DIR(nzb_dir): + results += ["Unable to create directory " + os.path.normpath(nzb_dir) + ", dir not changed."] + + if not config.change_TORRENT_DIR(torrent_dir): + results += ["Unable to create directory " + os.path.normpath(torrent_dir) + ", dir not changed."] + + config.change_SEARCH_FREQUENCY(search_frequency) + + if download_propers == "on": + download_propers = 1 + else: + download_propers = 0 + + if use_nzbs == "on": + use_nzbs = 1 + else: + use_nzbs = 0 + + if use_torrents == "on": + use_torrents = 1 + else: + use_torrents = 0 + + if usenet_retention == None: + usenet_retention = 200 + + sickbeard.USE_NZBS = use_nzbs + sickbeard.USE_TORRENTS = use_torrents + + sickbeard.NZB_METHOD = nzb_method + sickbeard.USENET_RETENTION = int(usenet_retention) + + sickbeard.DOWNLOAD_PROPERS = download_propers + + sickbeard.SAB_USERNAME = sab_username + sickbeard.SAB_PASSWORD = sab_password + sickbeard.SAB_APIKEY = sab_apikey.strip() + sickbeard.SAB_CATEGORY = sab_category + + if sab_host and not re.match('https?://.*', sab_host): + sab_host = 'http://' + sab_host + + if not sab_host.endswith('/'): + sab_host = sab_host + '/' + + sickbeard.SAB_HOST = sab_host + + sickbeard.NZBGET_PASSWORD = nzbget_password + sickbeard.NZBGET_CATEGORY = nzbget_category + sickbeard.NZBGET_HOST = nzbget_host + + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/search/") + +class ConfigPostProcessing: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config_postProcessing.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, + xbmc_data=None, mediabrowser_data=None, synology_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, + use_banner=None, keep_processed_dir=None, process_automatically=None, rename_episodes=None, + move_associated_files=None, tv_download_dir=None, naming_custom_abd=None, naming_abd_pattern=None): + + results = [] + + if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): + results += ["Unable to create directory " + os.path.normpath(tv_download_dir) + ", dir not changed."] + + if use_banner == "on": + use_banner = 1 + else: + use_banner = 0 + + if process_automatically == "on": + process_automatically = 1 + else: + process_automatically = 0 + + if rename_episodes == "on": + rename_episodes = 1 + else: + rename_episodes = 0 + + if keep_processed_dir == "on": + keep_processed_dir = 1 + else: + keep_processed_dir = 0 + + if move_associated_files == "on": + move_associated_files = 1 + else: + move_associated_files = 0 + + if naming_custom_abd == "on": + naming_custom_abd = 1 + else: + naming_custom_abd = 0 + + sickbeard.PROCESS_AUTOMATICALLY = process_automatically + sickbeard.KEEP_PROCESSED_DIR = keep_processed_dir + sickbeard.RENAME_EPISODES = rename_episodes + sickbeard.MOVE_ASSOCIATED_FILES = move_associated_files + sickbeard.NAMING_CUSTOM_ABD = naming_custom_abd + + sickbeard.metadata_provider_dict['XBMC'].set_config(xbmc_data) + sickbeard.metadata_provider_dict['MediaBrowser'].set_config(mediabrowser_data) + sickbeard.metadata_provider_dict['Synology'].set_config(synology_data) + sickbeard.metadata_provider_dict['Sony PS3'].set_config(sony_ps3_data) + sickbeard.metadata_provider_dict['WDTV'].set_config(wdtv_data) + sickbeard.metadata_provider_dict['TIVO'].set_config(tivo_data) + + if self.isNamingValid(naming_pattern, naming_multi_ep) != "invalid": + sickbeard.NAMING_PATTERN = naming_pattern + sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) + sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() + else: + results.append("You tried saving an invalid naming config, not saving your naming settings") + + if self.isNamingValid(naming_abd_pattern, None, True) != "invalid": + sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern + elif naming_custom_abd: + results.append("You tried saving an invalid air-by-date naming config, not saving your air-by-date settings") + + sickbeard.USE_BANNER = use_banner + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/postProcessing/") + + @cherrypy.expose + def testNaming(self, pattern=None, multi=None, abd=False): + + if multi != None: + multi = int(multi) + + result = naming.test_name(pattern, multi, abd) + + result = ek.ek(os.path.join, result['dir'], result['name']) + + return result + + @cherrypy.expose + def isNamingValid(self, pattern=None, multi=None, abd=False): + if pattern == None: + return "invalid" + + # air by date shows just need one check, we don't need to worry about season folders + if abd: + is_valid = naming.check_valid_abd_naming(pattern) + require_season_folders = False + + else: + # check validity of single and multi ep cases for the whole path + is_valid = naming.check_valid_naming(pattern, multi) + + # check validity of single and multi ep cases for only the file name + require_season_folders = naming.check_force_season_folders(pattern, multi) + + if is_valid and not require_season_folders: + return "valid" + elif is_valid and require_season_folders: + return "seasonfolders" + else: + return "invalid" + + +class ConfigProviders: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="config_providers.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def canAddNewznabProvider(self, name): + + if not name: + return json.dumps({'error': 'Invalid name specified'}) + + providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + tempProvider = newznab.NewznabProvider(name, '') + + if tempProvider.getID() in providerDict: + return json.dumps({'error': 'Exists as '+providerDict[tempProvider.getID()].name}) + else: + return json.dumps({'success': tempProvider.getID()}) + + @cherrypy.expose + def saveNewznabProvider(self, name, url, key=''): + + if not name or not url: + return '0' + + if not url.endswith('/'): + url = url + '/' + + providerDict = dict(zip([x.name for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + if name in providerDict: + if not providerDict[name].default: + providerDict[name].name = name + providerDict[name].url = url + providerDict[name].key = key + + return providerDict[name].getID() + '|' + providerDict[name].configStr() + + else: + + newProvider = newznab.NewznabProvider(name, url, key) + sickbeard.newznabProviderList.append(newProvider) + return newProvider.getID() + '|' + newProvider.configStr() + + + + @cherrypy.expose + def deleteNewznabProvider(self, id): + + providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + if id not in providerDict or providerDict[id].default: + return '0' + + # delete it from the list + sickbeard.newznabProviderList.remove(providerDict[id]) + + if id in sickbeard.PROVIDER_ORDER: + sickbeard.PROVIDER_ORDER.remove(id) + + return '1' + + + @cherrypy.expose + def saveProviders(self, nzbmatrix_username=None, nzbmatrix_apikey=None, + nzbs_r_us_uid=None, nzbs_r_us_hash=None, newznab_string=None, + tvtorrents_digest=None, tvtorrents_hash=None, + btn_api_key=None, binnewz_language=None, + newzbin_username=None, newzbin_password=None,t411_language=None,t411_username=None,t411_password=None, + provider_order=None): + + results = [] + + provider_str_list = provider_order.split() + provider_list = [] + + newznabProviderDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + finishedNames = [] + + # add all the newznab info we got into our list + for curNewznabProviderStr in newznab_string.split('!!!'): + + if not curNewznabProviderStr: + continue + + curName, curURL, curKey = curNewznabProviderStr.split('|') + + newProvider = newznab.NewznabProvider(curName, curURL, curKey) + + curID = newProvider.getID() + + # if it already exists then update it + if curID in newznabProviderDict: + newznabProviderDict[curID].name = curName + newznabProviderDict[curID].url = curURL + newznabProviderDict[curID].key = curKey + else: + sickbeard.newznabProviderList.append(newProvider) + + finishedNames.append(curID) + + # delete anything that is missing + for curProvider in sickbeard.newznabProviderList: + if curProvider.getID() not in finishedNames: + sickbeard.newznabProviderList.remove(curProvider) + + # do the enable/disable + for curProviderStr in provider_str_list: + curProvider, curEnabled = curProviderStr.split(':') + curEnabled = int(curEnabled) + + provider_list.append(curProvider) + + if curProvider == 'nzbs_r_us': + sickbeard.NZBSRUS = curEnabled + elif curProvider == 'nzbs_org_old': + sickbeard.NZBS = curEnabled + elif curProvider == 'nzbmatrix': + sickbeard.NZBMATRIX = curEnabled + elif curProvider == 'newzbin': + sickbeard.NEWZBIN = curEnabled + elif curProvider == 'bin_req': + sickbeard.BINREQ = curEnabled + elif curProvider == 'womble_s_index': + sickbeard.WOMBLE = curEnabled + elif curProvider == 'ezrss': + sickbeard.EZRSS = curEnabled + elif curProvider == 'tvtorrents': + sickbeard.TVTORRENTS = curEnabled + elif curProvider == 'btn': + sickbeard.BTN = curEnabled + elif curProvider == 'binnewz': + sickbeard.BINNEWZ = curEnabled + elif curProvider == 't411': + sickbeard.T411 = curEnabled + elif curProvider in newznabProviderDict: + newznabProviderDict[curProvider].enabled = bool(curEnabled) + else: + logger.log(u"don't know what "+curProvider+" is, skipping") + + sickbeard.TVTORRENTS_DIGEST = tvtorrents_digest.strip() + sickbeard.TVTORRENTS_HASH = tvtorrents_hash.strip() + + sickbeard.BTN_API_KEY = btn_api_key.strip() + + sickbeard.BINNEWZ_LANGUAGE = binnewz_language + + sickbeard.T411_LANGUAGE = t411_language + sickbeard.T411_USERNAME = t411_username + sickbeard.T411_PASSWORD = t411_password + + sickbeard.NZBSRUS_UID = nzbs_r_us_uid.strip() + sickbeard.NZBSRUS_HASH = nzbs_r_us_hash.strip() + + sickbeard.PROVIDER_ORDER = provider_list + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/providers/") + +class ConfigNotifications: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="config_notifications.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveNotifications(self, use_xbmc=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, + xbmc_update_library=None, xbmc_update_full=None, xbmc_host=None, xbmc_username=None, xbmc_password=None, + use_plex=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, plex_update_library=None, + plex_server_host=None, plex_host=None, plex_username=None, plex_password=None, + use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, growl_host=None, growl_password=None, + use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, prowl_api=None, prowl_priority=0, + use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, + use_notifo=None, notifo_notify_onsnatch=None, notifo_notify_ondownload=None, notifo_username=None, notifo_apisecret=None, + use_boxcar=None, boxcar_notify_onsnatch=None, boxcar_notify_ondownload=None, boxcar_username=None, + use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, pushover_userkey=None, + use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, + use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, + use_trakt=None, trakt_username=None, trakt_password=None, trakt_api=None, + use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, pytivo_update_library=None, + pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, + use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, nma_api=None, nma_priority=0 ): + + results = [] + + if xbmc_notify_onsnatch == "on": + xbmc_notify_onsnatch = 1 + else: + xbmc_notify_onsnatch = 0 + + if xbmc_notify_ondownload == "on": + xbmc_notify_ondownload = 1 + else: + xbmc_notify_ondownload = 0 + + if xbmc_update_library == "on": + xbmc_update_library = 1 + else: + xbmc_update_library = 0 + + if xbmc_update_full == "on": + xbmc_update_full = 1 + else: + xbmc_update_full = 0 + + if use_xbmc == "on": + use_xbmc = 1 + else: + use_xbmc = 0 + + if plex_update_library == "on": + plex_update_library = 1 + else: + plex_update_library = 0 + + if plex_notify_onsnatch == "on": + plex_notify_onsnatch = 1 + else: + plex_notify_onsnatch = 0 + + if plex_notify_ondownload == "on": + plex_notify_ondownload = 1 + else: + plex_notify_ondownload = 0 + + if use_plex == "on": + use_plex = 1 + else: + use_plex = 0 + + if growl_notify_onsnatch == "on": + growl_notify_onsnatch = 1 + else: + growl_notify_onsnatch = 0 + + if growl_notify_ondownload == "on": + growl_notify_ondownload = 1 + else: + growl_notify_ondownload = 0 + + if use_growl == "on": + use_growl = 1 + else: + use_growl = 0 + + if prowl_notify_onsnatch == "on": + prowl_notify_onsnatch = 1 + else: + prowl_notify_onsnatch = 0 + + if prowl_notify_ondownload == "on": + prowl_notify_ondownload = 1 + else: + prowl_notify_ondownload = 0 + if use_prowl == "on": + use_prowl = 1 + else: + use_prowl = 0 + + if twitter_notify_onsnatch == "on": + twitter_notify_onsnatch = 1 + else: + twitter_notify_onsnatch = 0 + + if twitter_notify_ondownload == "on": + twitter_notify_ondownload = 1 + else: + twitter_notify_ondownload = 0 + if use_twitter == "on": + use_twitter = 1 + else: + use_twitter = 0 + + if notifo_notify_onsnatch == "on": + notifo_notify_onsnatch = 1 + else: + notifo_notify_onsnatch = 0 + + if notifo_notify_ondownload == "on": + notifo_notify_ondownload = 1 + else: + notifo_notify_ondownload = 0 + if use_notifo == "on": + use_notifo = 1 + else: + use_notifo = 0 + + if boxcar_notify_onsnatch == "on": + boxcar_notify_onsnatch = 1 + else: + boxcar_notify_onsnatch = 0 + + if boxcar_notify_ondownload == "on": + boxcar_notify_ondownload = 1 + else: + boxcar_notify_ondownload = 0 + if use_boxcar == "on": + use_boxcar = 1 + else: + use_boxcar = 0 + + if pushover_notify_onsnatch == "on": + pushover_notify_onsnatch = 1 + else: + pushover_notify_onsnatch = 0 + + if pushover_notify_ondownload == "on": + pushover_notify_ondownload = 1 + else: + pushover_notify_ondownload = 0 + if use_pushover == "on": + use_pushover = 1 + else: + use_pushover = 0 + + if use_nmj == "on": + use_nmj = 1 + else: + use_nmj = 0 + + if use_synoindex == "on": + use_synoindex = 1 + else: + use_synoindex = 0 + + if use_trakt == "on": + use_trakt = 1 + else: + use_trakt = 0 + + if use_pytivo == "on": + use_pytivo = 1 + else: + use_pytivo = 0 + + if pytivo_notify_onsnatch == "on": + pytivo_notify_onsnatch = 1 + else: + pytivo_notify_onsnatch = 0 + + if pytivo_notify_ondownload == "on": + pytivo_notify_ondownload = 1 + else: + pytivo_notify_ondownload = 0 + + if pytivo_update_library == "on": + pytivo_update_library = 1 + else: + pytivo_update_library = 0 + + if use_nma == "on": + use_nma = 1 + else: + use_nma = 0 + + if nma_notify_onsnatch == "on": + nma_notify_onsnatch = 1 + else: + nma_notify_onsnatch = 0 + + if nma_notify_ondownload == "on": + nma_notify_ondownload = 1 + else: + nma_notify_ondownload = 0 + + sickbeard.USE_XBMC = use_xbmc + sickbeard.XBMC_NOTIFY_ONSNATCH = xbmc_notify_onsnatch + sickbeard.XBMC_NOTIFY_ONDOWNLOAD = xbmc_notify_ondownload + sickbeard.XBMC_UPDATE_LIBRARY = xbmc_update_library + sickbeard.XBMC_UPDATE_FULL = xbmc_update_full + sickbeard.XBMC_HOST = xbmc_host + sickbeard.XBMC_USERNAME = xbmc_username + sickbeard.XBMC_PASSWORD = xbmc_password + + sickbeard.USE_PLEX = use_plex + sickbeard.PLEX_NOTIFY_ONSNATCH = plex_notify_onsnatch + sickbeard.PLEX_NOTIFY_ONDOWNLOAD = plex_notify_ondownload + sickbeard.PLEX_UPDATE_LIBRARY = plex_update_library + sickbeard.PLEX_HOST = plex_host + sickbeard.PLEX_SERVER_HOST = plex_server_host + sickbeard.PLEX_USERNAME = plex_username + sickbeard.PLEX_PASSWORD = plex_password + + sickbeard.USE_GROWL = use_growl + sickbeard.GROWL_NOTIFY_ONSNATCH = growl_notify_onsnatch + sickbeard.GROWL_NOTIFY_ONDOWNLOAD = growl_notify_ondownload + sickbeard.GROWL_HOST = growl_host + sickbeard.GROWL_PASSWORD = growl_password + + sickbeard.USE_PROWL = use_prowl + sickbeard.PROWL_NOTIFY_ONSNATCH = prowl_notify_onsnatch + sickbeard.PROWL_NOTIFY_ONDOWNLOAD = prowl_notify_ondownload + sickbeard.PROWL_API = prowl_api + sickbeard.PROWL_PRIORITY = prowl_priority + + sickbeard.USE_TWITTER = use_twitter + sickbeard.TWITTER_NOTIFY_ONSNATCH = twitter_notify_onsnatch + sickbeard.TWITTER_NOTIFY_ONDOWNLOAD = twitter_notify_ondownload + + sickbeard.USE_NOTIFO = use_notifo + sickbeard.NOTIFO_NOTIFY_ONSNATCH = notifo_notify_onsnatch + sickbeard.NOTIFO_NOTIFY_ONDOWNLOAD = notifo_notify_ondownload + sickbeard.NOTIFO_USERNAME = notifo_username + sickbeard.NOTIFO_APISECRET = notifo_apisecret + + sickbeard.USE_BOXCAR = use_boxcar + sickbeard.BOXCAR_NOTIFY_ONSNATCH = boxcar_notify_onsnatch + sickbeard.BOXCAR_NOTIFY_ONDOWNLOAD = boxcar_notify_ondownload + sickbeard.BOXCAR_USERNAME = boxcar_username + + sickbeard.USE_PUSHOVER = use_pushover + sickbeard.PUSHOVER_NOTIFY_ONSNATCH = pushover_notify_onsnatch + sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD = pushover_notify_ondownload + sickbeard.PUSHOVER_USERKEY = pushover_userkey + + sickbeard.USE_LIBNOTIFY = use_libnotify == "on" + sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH = libnotify_notify_onsnatch == "on" + sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD = libnotify_notify_ondownload == "on" + + sickbeard.USE_NMJ = use_nmj + sickbeard.NMJ_HOST = nmj_host + sickbeard.NMJ_DATABASE = nmj_database + sickbeard.NMJ_MOUNT = nmj_mount + + sickbeard.USE_SYNOINDEX = use_synoindex + + sickbeard.USE_TRAKT = use_trakt + sickbeard.TRAKT_USERNAME = trakt_username + sickbeard.TRAKT_PASSWORD = trakt_password + sickbeard.TRAKT_API = trakt_api + + sickbeard.USE_PYTIVO = use_pytivo + sickbeard.PYTIVO_NOTIFY_ONSNATCH = pytivo_notify_onsnatch == "off" + sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = pytivo_notify_ondownload == "off" + sickbeard.PYTIVO_UPDATE_LIBRARY = pytivo_update_library + sickbeard.PYTIVO_HOST = pytivo_host + sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name + sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name + + sickbeard.USE_NMA = use_nma + sickbeard.NMA_NOTIFY_ONSNATCH = nma_notify_onsnatch + sickbeard.NMA_NOTIFY_ONDOWNLOAD = nma_notify_ondownload + sickbeard.NMA_API = nma_api + sickbeard.NMA_PRIORITY = nma_priority + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/notifications/") + + +class Config: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + general = ConfigGeneral() + + search = ConfigSearch() + + postProcessing = ConfigPostProcessing() + + providers = ConfigProviders() + + notifications = ConfigNotifications() + +def haveXBMC(): + return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY + +def havePLEX(): + return sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY + +def HomeMenu(): + return [ + { 'title': 'Add Shows', 'path': 'home/addShows/', }, + { 'title': 'Manual Post-Processing', 'path': 'home/postprocess/' }, + { 'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': haveXBMC }, + { 'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': havePLEX }, + { 'title': 'Restart', 'path': 'home/restart/?pid='+str(sickbeard.PID), 'confirm': True }, + { 'title': 'Shutdown', 'path': 'home/shutdown/?pid='+str(sickbeard.PID), 'confirm': True }, + ] + +class HomePostProcess: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="home_postprocess.tmpl") + t.submenu = HomeMenu() + return _munge(t) + + @cherrypy.expose + def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None): + + if not dir: + redirect("/home/postprocess") + else: + result = processTV.processDir(dir, nzbName) + if quiet != None and int(quiet) == 1: + return result + + result = result.replace("\n","<br />\n") + return _genericMessage("Postprocessing results", result) + + +class NewHomeAddShows: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="home_addShows.tmpl") + t.submenu = HomeMenu() + return _munge(t) + + @cherrypy.expose + def getTVDBLanguages(self): + result = tvdb_api.Tvdb().config['valid_languages'] + + # Make sure list is sorted alphabetically but 'en' is in front + if 'en' in result: + del result[result.index('en')] + result.sort() + result.insert(0, 'en') + + return json.dumps({'results': result}) + + @cherrypy.expose + def sanitizeFileName(self, name): + return helpers.sanitizeFileName(name) + + @cherrypy.expose + def searchTVDBForShowName(self, name, lang="en"): + if not lang or lang == 'null': + lang = "en" + + baseURL = "http://www.thetvdb.com/api/GetSeries.php?" + nameUTF8 = name.encode('utf-8') + + logger.log(u"Trying to find Show on thetvdb.com with: " + nameUTF8.decode('utf-8'), logger.DEBUG) + + # Use each word in the show's name as a possible search term + keywords = nameUTF8.split(' ') + + # Insert the whole show's name as the first search term so best results are first + # ex: keywords = ['Some Show Name', 'Some', 'Show', 'Name'] + if len(keywords) > 1: + keywords.insert(0, nameUTF8) + + # Query the TVDB for each search term and build the list of results + results = [] + + for searchTerm in keywords: + params = {'seriesname': searchTerm, + 'language': lang} + + finalURL = baseURL + urllib.urlencode(params) + + logger.log(u"Searching for Show with searchterm: \'" + searchTerm.decode('utf-8') + u"\' on URL " + finalURL, logger.DEBUG) + urlData = helpers.getURL(finalURL) + + if urlData is None: + # When urlData is None, trouble connecting to TVDB, don't try the rest of the keywords + logger.log(u"Unable to get URL: " + finalURL, logger.ERROR) + break + else: + try: + seriesXML = etree.ElementTree(etree.XML(urlData)) + series = seriesXML.getiterator('Series') + + except Exception, e: + # use finalURL in log, because urlData can be too much information + logger.log(u"Unable to parse XML for some reason: " + ex(e) + " from XML: " + finalURL, logger.ERROR) + series = '' + + # add each result to our list + for curSeries in series: + tvdb_id = int(curSeries.findtext('seriesid')) + + # don't add duplicates + if tvdb_id in [x[0] for x in results]: + continue + + results.append((tvdb_id, curSeries.findtext('SeriesName'), curSeries.findtext('FirstAired'))) + + lang_id = tvdb_api.Tvdb().config['langabbv_to_id'][lang] + + return json.dumps({'results': results, 'langid': lang_id}) + + @cherrypy.expose + def massAddTable(self, rootDir=None): + t = PageTemplate(file="home_massAddTable.tmpl") + t.submenu = HomeMenu() + + myDB = db.DBConnection() + + if not rootDir: + return "No folders selected." + elif type(rootDir) != list: + root_dirs = [rootDir] + else: + root_dirs = rootDir + + root_dirs = [urllib.unquote_plus(x) for x in root_dirs] + + default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) + if len(root_dirs) > default_index: + tmp = root_dirs[default_index] + if tmp in root_dirs: + root_dirs.remove(tmp) + root_dirs = [tmp]+root_dirs + + dir_list = [] + + for root_dir in root_dirs: + try: + file_list = ek.ek(os.listdir, root_dir) + except: + continue + + for cur_file in file_list: + + cur_path = ek.ek(os.path.normpath, ek.ek(os.path.join, root_dir, cur_file)) + if not ek.ek(os.path.isdir, cur_path): + continue + + cur_dir = { + 'dir': cur_path, + 'display_dir': '<b>'+ek.ek(os.path.dirname, cur_path)+os.sep+'</b>'+ek.ek(os.path.basename, cur_path), + } + + # see if the folder is in XBMC already + dirResults = myDB.select("SELECT * FROM tv_shows WHERE location = ?", [cur_path]) + + if dirResults: + cur_dir['added_already'] = True + else: + cur_dir['added_already'] = False + + dir_list.append(cur_dir) + + tvdb_id = '' + show_name = '' + for cur_provider in sickbeard.metadata_provider_dict.values(): + (tvdb_id, show_name) = cur_provider.retrieveShowMetadata(cur_path) + if tvdb_id and show_name: + break + + cur_dir['existing_info'] = (tvdb_id, show_name) + + if tvdb_id and helpers.findCertainShow(sickbeard.showList, tvdb_id): + cur_dir['added_already'] = True + + t.dirList = dir_list + + return _munge(t) + + @cherrypy.expose + def newShow(self, show_to_add=None, other_shows=None): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + t = PageTemplate(file="home_newShow.tmpl") + t.submenu = HomeMenu() + + show_dir, tvdb_id, show_name = self.split_extra_show(show_to_add) + + if tvdb_id and show_name: + use_provided_info = True + else: + use_provided_info = False + + # tell the template whether we're giving it show name & TVDB ID + t.use_provided_info = use_provided_info + + # use the given show_dir for the tvdb search if available + if not show_dir: + t.default_show_name = '' + elif not show_name: + t.default_show_name = ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.',' ') + else: + t.default_show_name = show_name + + # carry a list of other dirs if given + if not other_shows: + other_shows = [] + elif type(other_shows) != list: + other_shows = [other_shows] + + if use_provided_info: + t.provided_tvdb_id = tvdb_id + t.provided_tvdb_name = show_name + + t.provided_show_dir = show_dir + t.other_shows = other_shows + + return _munge(t) + + @cherrypy.expose + def addNewShow(self, whichSeries=None, tvdbLang="en", rootDir=None, defaultStatus=None, + anyQualities=None, bestQualities=None, flatten_folders=None, fullShowPath=None, + other_shows=None, skipShow=None,showLang=None): + """ + Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are + provided then it forwards back to newShow, if not it goes to /home. + """ + + # grab our list of other dirs if given + if not other_shows: + other_shows = [] + elif type(other_shows) != list: + other_shows = [other_shows] + + def finishAddShow(): + # if there are no extra shows then go home + if not other_shows: + redirect('/home') + + # peel off the next one + next_show_dir = other_shows[0] + rest_of_show_dirs = other_shows[1:] + + # go to add the next show + return self.newShow(next_show_dir, rest_of_show_dirs) + + # if we're skipping then behave accordingly + if skipShow: + return finishAddShow() + + # sanity check on our inputs + if (not rootDir and not fullShowPath) or not whichSeries: + return "Missing params, no tvdb id or folder:"+repr(whichSeries)+" and "+repr(rootDir)+"/"+repr(fullShowPath) + + # figure out what show we're adding and where + series_pieces = whichSeries.partition('|') + if len(series_pieces) < 3: + return "Error with show selection." + + tvdb_id = int(series_pieces[0]) + show_name = series_pieces[2] + + # use the whole path if it's given, or else append the show name to the root dir to get the full show path + if fullShowPath: + show_dir = ek.ek(os.path.normpath, fullShowPath) + else: + show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) + + # blanket policy - if the dir exists you should have used "add existing show" numbnuts + if ek.ek(os.path.isdir, show_dir) and not fullShowPath: + ui.notifications.error("Unable to add show", "Folder "+show_dir+" exists already") + redirect('/home/addShows/existingShows') + + # don't create show dir if config says not to + if sickbeard.ADD_SHOWS_WO_DIR: + logger.log(u"Skipping initial creation of "+show_dir+" due to config.ini setting") + else: + dir_exists = helpers.makeDir(show_dir) + if not dir_exists: + logger.log(u"Unable to create the folder "+show_dir+", can't add the show", logger.ERROR) + ui.notifications.error("Unable to add show", "Unable to create the folder "+show_dir+", can't add the show") + redirect("/home") + else: + helpers.chmodAsParent(show_dir) + + # prepare the inputs for passing along + if flatten_folders == "on": + flatten_folders = 1 + else: + flatten_folders = 0 + + if not anyQualities: + anyQualities = [] + if not bestQualities: + bestQualities = [] + if type(anyQualities) != list: + anyQualities = [anyQualities] + if type(bestQualities) != list: + bestQualities = [bestQualities] + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + + # add the show + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, flatten_folders, tvdbLang,showLang) #@UndefinedVariable + ui.notifications.message('Show added', 'Adding the specified show into '+show_dir) + + return finishAddShow() + + + @cherrypy.expose + def existingShows(self): + """ + Prints out the page to add existing shows from a root dir + """ + t = PageTemplate(file="home_addExistingShow.tmpl") + t.submenu = HomeMenu() + + return _munge(t) + + def split_extra_show(self, extra_show): + if not extra_show: + return (None, None, None) + split_vals = extra_show.split('|') + if len(split_vals) < 3: + return (extra_show, None, None) + show_dir = split_vals[0] + tvdb_id = split_vals[1] + show_name = '|'.join(split_vals[2:]) + + return (show_dir, tvdb_id, show_name) + + @cherrypy.expose + def addExistingShows(self, shows_to_add=None, promptForSettings=None): + """ + Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards + along to the newShow page. + """ + + # grab a list of other shows to add, if provided + if not shows_to_add: + shows_to_add = [] + elif type(shows_to_add) != list: + shows_to_add = [shows_to_add] + + shows_to_add = [urllib.unquote_plus(x) for x in shows_to_add] + + if promptForSettings == "on": + promptForSettings = 1 + else: + promptForSettings = 0 + + tvdb_id_given = [] + dirs_only = [] + # separate all the ones with TVDB IDs + for cur_dir in shows_to_add: + if not '|' in cur_dir: + dirs_only.append(cur_dir) + else: + show_dir, tvdb_id, show_name = self.split_extra_show(cur_dir) + if not show_dir or not tvdb_id or not show_name: + continue + tvdb_id_given.append((show_dir, int(tvdb_id), show_name)) + + + # if they want me to prompt for settings then I will just carry on to the newShow page + if promptForSettings and shows_to_add: + return self.newShow(shows_to_add[0], shows_to_add[1:]) + + # if they don't want me to prompt for settings then I can just add all the nfo shows now + num_added = 0 + for cur_show in tvdb_id_given: + show_dir, tvdb_id, show_name = cur_show + + # add the show + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.FLATTEN_FOLDERS_DEFAULT) #@UndefinedVariable + num_added += 1 + + if num_added: + ui.notifications.message("Shows Added", "Automatically added "+str(num_added)+" from their existing metadata files") + + # if we're done then go home + if not dirs_only: + redirect('/home') + + # for the remaining shows we need to prompt for each one, so forward this on to the newShow page + return self.newShow(dirs_only[0], dirs_only[1:]) + + + + +ErrorLogsMenu = [ + { 'title': 'Clear Errors', 'path': 'errorlogs/clearerrors' }, + #{ 'title': 'View Log', 'path': 'errorlogs/viewlog' }, +] + + +class ErrorLogs: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="errorlogs.tmpl") + t.submenu = ErrorLogsMenu + + return _munge(t) + + + @cherrypy.expose + def clearerrors(self): + classes.ErrorViewer.clear() + redirect("/errorlogs") + + @cherrypy.expose + def viewlog(self, minLevel=logger.MESSAGE, maxLines=500): + + t = PageTemplate(file="viewlogs.tmpl") + t.submenu = ErrorLogsMenu + + minLevel = int(minLevel) + + data = [] + if os.path.isfile(logger.sb_log_instance.log_file): + f = ek.ek(open, logger.sb_log_instance.log_file) + data = f.readlines() + f.close() + + regex = "^(\w{3})\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$" + + finalData = [] + + numLines = 0 + lastLine = False + numToShow = min(maxLines, len(data)) + + for x in reversed(data): + + x = x.decode('utf-8') + match = re.match(regex, x) + + if match: + level = match.group(6) + if level not in logger.reverseNames: + lastLine = False + continue + + if logger.reverseNames[level] >= minLevel: + lastLine = True + finalData.append(x) + else: + lastLine = False + continue + + elif lastLine: + finalData.append("AA"+x) + + numLines += 1 + + if numLines >= numToShow: + break + + result = "".join(finalData) + + t.logLines = result + t.minLevel = minLevel + + return _munge(t) + + +class Home: + + @cherrypy.expose + def is_alive(self, *args, **kwargs): + if 'callback' in kwargs and '_' in kwargs: + callback, _ = kwargs['callback'], kwargs['_'] + else: + return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query stiring." + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + cherrypy.response.headers['Content-Type'] = 'text/javascript' + cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' + cherrypy.response.headers['Access-Control-Allow-Headers'] = 'x-requested-with' + + if sickbeard.started: + return callback+'('+json.dumps({"msg": str(sickbeard.PID)})+');' + else: + return callback+'('+json.dumps({"msg": "nope"})+');' + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="home.tmpl") + t.submenu = HomeMenu() + return _munge(t) + + addShows = NewHomeAddShows() + + postprocess = HomePostProcess() + + @cherrypy.expose + def testSABnzbd(self, host=None, username=None, password=None, apikey=None): + if not host.endswith("/"): + host = host + "/" + connection, accesMsg = sab.getSabAccesMethod(host, username, password, apikey) + if connection: + authed, authMsg = sab.testAuthentication(host, username, password, apikey) #@UnusedVariable + if authed: + return "Success. Connected and authenticated" + else: + return "Authentication failed. SABnzbd expects '"+accesMsg+"' as authentication method" + else: + return "Unable to connect to host" + + @cherrypy.expose + def testGrowl(self, host=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.growl_notifier.test_notify(host, password) + if password==None or password=='': + pw_append = '' + else: + pw_append = " with password: " + password + + if result: + return "Registered and Tested growl successfully "+urllib.unquote_plus(host)+pw_append + else: + return "Registration and Testing of growl failed "+urllib.unquote_plus(host)+pw_append + + @cherrypy.expose + def testProwl(self, prowl_api=None, prowl_priority=0): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) + if result: + return "Test prowl notice sent successfully" + else: + return "Test prowl notice failed" + + @cherrypy.expose + def testNotifo(self, username=None, apisecret=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.notifo_notifier.test_notify(username, apisecret) + if result: + return "Notifo notification succeeded. Check your Notifo clients to make sure it worked" + else: + return "Error sending Notifo notification" + + @cherrypy.expose + def testBoxcar(self, username=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.boxcar_notifier.test_notify(username) + if result: + return "Boxcar notification succeeded. Check your Boxcar clients to make sure it worked" + else: + return "Error sending Boxcar notification" + + @cherrypy.expose + def testPushover(self, userKey=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.pushover_notifier.test_notify(userKey) + if result: + return "Pushover notification succeeded. Check your Pushover clients to make sure it worked" + else: + return "Error sending Pushover notification" + + @cherrypy.expose + def twitterStep1(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + return notifiers.twitter_notifier._get_authorization() + + @cherrypy.expose + def twitterStep2(self, key): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.twitter_notifier._get_credentials(key) + logger.log(u"result: "+str(result)) + if result: + return "Key verification successful" + else: + return "Unable to verify key" + + @cherrypy.expose + def testTwitter(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.twitter_notifier.test_notify() + if result: + return "Tweet successful, check your twitter to make sure it worked" + else: + return "Error sending tweet" + + @cherrypy.expose + def testXBMC(self, host=None, username=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + finalResult = '' + for curHost in [x.strip() for x in host.split(",")]: + curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) + if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: + finalResult += "Test XBMC notice sent successfully to " + urllib.unquote_plus(curHost) + else: + finalResult += "Test XBMC notice failed to " + urllib.unquote_plus(curHost) + finalResult += "<br />\n" + + return finalResult + + @cherrypy.expose + def testPLEX(self, host=None, username=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + finalResult = '' + for curHost in [x.strip() for x in host.split(",")]: + curResult = notifiers.plex_notifier.test_notify(urllib.unquote_plus(curHost), username, password) + if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: + finalResult += "Test Plex notice sent successfully to " + urllib.unquote_plus(curHost) + else: + finalResult += "Test Plex notice failed to " + urllib.unquote_plus(curHost) + finalResult += "<br />\n" + + return finalResult + + @cherrypy.expose + def testLibnotify(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + if notifiers.libnotify_notifier.test_notify(): + return "Tried sending desktop notification via libnotify" + else: + return notifiers.libnotify.diagnose() + + @cherrypy.expose + def testNMJ(self, host=None, database=None, mount=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) + if result: + return "Successfull started the scan update" + else: + return "Test failed to start the scan update" + + @cherrypy.expose + def settingsNMJ(self, host=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) + if result: + return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % {"host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} + else: + return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' + + @cherrypy.expose + def testTrakt(self, api=None, username=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.trakt_notifier.test_notify(api, username, password) + if result: + return "Test notice sent successfully to Trakt" + else: + return "Test notice failed to Trakt" + + @cherrypy.expose + def testNMA(self, nma_api=None, nma_priority=0): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) + if result: + return "Test NMA notice sent successfully" + else: + return "Test NMA notice failed" + + @cherrypy.expose + def shutdown(self, pid=None): + + if str(pid) != str(sickbeard.PID): + redirect("/home") + + threading.Timer(2, sickbeard.invoke_shutdown).start() + + title = "Shutting down" + message = "Sick Beard is shutting down..." + + return _genericMessage(title, message) + + @cherrypy.expose + def restart(self, pid=None): + + if str(pid) != str(sickbeard.PID): + redirect("/home") + + t = PageTemplate(file="restart.tmpl") + t.submenu = HomeMenu() + + # do a soft restart + threading.Timer(2, sickbeard.invoke_restart, [False]).start() + + return _munge(t) + + @cherrypy.expose + def update(self, pid=None): + + if str(pid) != str(sickbeard.PID): + redirect("/home") + + updated = sickbeard.versionCheckScheduler.action.update() #@UndefinedVariable + + if updated: + # do a hard restart + threading.Timer(2, sickbeard.invoke_restart, [False]).start() + t = PageTemplate(file="restart_bare.tmpl") + return _munge(t) + else: + return _genericMessage("Update Failed","Update wasn't successful, not restarting. Check your log for more information.") + + @cherrypy.expose + def displayShow(self, show=None): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + else: + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Show not in show list") + + myDB = db.DBConnection() + + seasonResults = myDB.select( + "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season desc", + [showObj.tvdbid] + ) + + sqlResults = myDB.select( + "SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", + [showObj.tvdbid] + ) + + t = PageTemplate(file="displayShow.tmpl") + t.submenu = [ { 'title': 'Edit', 'path': 'home/editShow?show=%d'%showObj.tvdbid } ] + + try: + t.showLoc = (showObj.location, True) + except sickbeard.exceptions.ShowDirNotFoundException: + t.showLoc = (showObj._location, False) + + show_message = '' + + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable + show_message = 'This show is in the process of being downloaded from theTVDB.com - the info below is incomplete.' + + elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + show_message = 'The information below is in the process of being updated.' + + elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): #@UndefinedVariable + show_message = 'The episodes below are currently being refreshed from disk' + + elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued to be refreshed.' + + elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued and awaiting an update.' + + if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable + if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + t.submenu.append({ 'title': 'Delete', 'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True }) + t.submenu.append({ 'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d'%showObj.tvdbid }) + t.submenu.append({ 'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1'%showObj.tvdbid }) + t.submenu.append({ 'title': 'Update show in XBMC', 'path': 'home/updateXBMC?showName=%s'%urllib.quote_plus(showObj.name.encode('utf-8')), 'requires': haveXBMC }) + t.submenu.append({ 'title': 'Preview Rename', 'path': 'home/testRename?show=%d'%showObj.tvdbid }) + + t.show = showObj + t.sqlResults = sqlResults + t.seasonResults = seasonResults + t.show_message = show_message + + epCounts = {} + epCats = {} + epCounts[Overview.SKIPPED] = 0 + epCounts[Overview.WANTED] = 0 + epCounts[Overview.QUAL] = 0 + epCounts[Overview.GOOD] = 0 + epCounts[Overview.UNAIRED] = 0 + + for curResult in sqlResults: + + curEpCat = showObj.getOverview(int(curResult["status"])) + epCats[str(curResult["season"])+"x"+str(curResult["episode"])] = curEpCat + epCounts[curEpCat] += 1 + + def titler(x): + if not x: + return x + if x.lower().startswith('a '): + x = x[2:] + elif x.lower().startswith('the '): + x = x[4:] + return x + t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) + + t.epCounts = epCounts + t.epCats = epCats + + return _munge(t) + + @cherrypy.expose + def plotDetails(self, show, season, episode): + result = db.DBConnection().action("SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", (show, season, episode)).fetchone() + return result['description'] if result else 'Episode not found.' + + @cherrypy.expose + def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], flatten_folders=None, paused=None, directCall=False, air_by_date=None, tvdbLang=None, audio_lang=None, custom_search_names=None): + + if show == None: + errString = "Invalid show ID: "+str(show) + if directCall: + return [errString] + else: + return _genericMessage("Error", errString) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + errString = "Unable to find the specified show: "+str(show) + if directCall: + return [errString] + else: + return _genericMessage("Error", errString) + + if not location and not anyQualities and not bestQualities and not flatten_folders: + + t = PageTemplate(file="editShow.tmpl") + t.submenu = HomeMenu() + with showObj.lock: + t.show = showObj + + return _munge(t) + + if flatten_folders == "on": + flatten_folders = 1 + else: + flatten_folders = 0 + + logger.log(u"flatten folders: "+str(flatten_folders)) + + if paused == "on": + paused = 1 + else: + paused = 0 + + if air_by_date == "on": + air_by_date = 1 + else: + air_by_date = 0 + + if tvdbLang and tvdbLang in tvdb_api.Tvdb().config['valid_languages']: + tvdb_lang = tvdbLang + else: + tvdb_lang = showObj.lang + + # if we changed the language then kick off an update + if tvdb_lang == showObj.lang: + do_update = False + else: + do_update = True + + if audio_lang and audio_lang in showLanguages.keys(): + audio_lang = audio_lang + else: + audio_lang = showObj.audio_lang + + + + if type(anyQualities) != list: + anyQualities = [anyQualities] + + if type(bestQualities) != list: + bestQualities = [bestQualities] + + errors = [] + with showObj.lock: + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + showObj.quality = newQuality + + # reversed for now + if bool(showObj.flatten_folders) != bool(flatten_folders): + showObj.flatten_folders = flatten_folders + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + except exceptions.CantRefreshException, e: + errors.append("Unable to refresh this show: "+ex(e)) + + showObj.paused = paused + showObj.air_by_date = air_by_date + showObj.lang = tvdb_lang + showObj.audio_lang = audio_lang + showObj.custom_search_names = custom_search_names + + # if we change location clear the db of episodes, change it, write to db, and rescan + if os.path.normpath(showObj._location) != os.path.normpath(location): + logger.log(os.path.normpath(showObj._location)+" != "+os.path.normpath(location), logger.DEBUG) + if not ek.ek(os.path.isdir, location): + errors.append("New location <tt>%s</tt> does not exist" % location) + + # don't bother if we're going to update anyway + elif not do_update: + # change it + try: + showObj.location = location + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + except exceptions.CantRefreshException, e: + errors.append("Unable to refresh this show:"+ex(e)) + # grab updated info from TVDB + #showObj.loadEpisodesFromTVDB() + # rescan the episodes in the new folder + except exceptions.NoNFOException: + errors.append("The folder at <tt>%s</tt> doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in Sick Beard." % location) + + # save it to the DB + showObj.saveToDB() + + # force the update + if do_update: + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable + time.sleep(1) + except exceptions.CantUpdateException, e: + errors.append("Unable to force an update on the show.") + + if directCall: + return errors + + if len(errors) > 0: + ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), + '<ul>' + '\n'.join(['<li>%s</li>' % error for error in errors]) + "</ul>") + + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def deleteShow(self, show=None): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + return _genericMessage("Error", "Shows can't be deleted while they're being added or updated.") + + showObj.deleteShow() + + ui.notifications.message('<b>%s</b> has been deleted' % showObj.name) + redirect("/home") + + @cherrypy.expose + def refreshShow(self, show=None): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + # force the update from the DB + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + except exceptions.CantRefreshException, e: + ui.notifications.error("Unable to refresh this show.", + ex(e)) + + time.sleep(3) + + redirect("/home/displayShow?show="+str(showObj.tvdbid)) + + @cherrypy.expose + def updateShow(self, show=None, force=0): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + # force the update + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) #@UndefinedVariable + except exceptions.CantUpdateException, e: + ui.notifications.error("Unable to update this show.", + ex(e)) + + # just give it some time + time.sleep(3) + + redirect("/home/displayShow?show="+str(showObj.tvdbid)) + + @cherrypy.expose + def updateXBMC(self, showName=None): + # TODO: configure that each host can have different options / username / pw + # only send update to first host in the list -- workaround for xbmc sql backend users + firstHost = sickbeard.XBMC_HOST.split(",")[0].strip() + if notifiers.xbmc_notifier.update_library(showName=showName): + ui.notifications.message("Library update command sent to XBMC host: " + firstHost) + else: + ui.notifications.error("Unable to contact XBMC host: " + firstHost) + redirect('/home') + + @cherrypy.expose + def updatePLEX(self): + if notifiers.plex_notifier.update_library(): + ui.notifications.message("Library update command sent to Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) + else: + ui.notifications.error("Unable to contact Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) + redirect('/home') + + @cherrypy.expose + def setStatus(self, show=None, eps=None, status=None, direct=False): + + if show == None or eps == None or status == None: + errMsg = "You must specify a show and at least one episode" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + if not statusStrings.has_key(int(status)): + errMsg = "Invalid status" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + errMsg = "Error", "Show not in show list" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + segment_list = [] + + if eps != None: + + for curEp in eps.split('|'): + + logger.log(u"Attempting to set status on episode "+curEp+" to "+status, logger.DEBUG) + + epInfo = curEp.split('x') + + epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) + + if int(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) + + if epObj == None: + return _genericMessage("Error", "Episode couldn't be retrieved") + + with epObj.lock: + # don't let them mess up UNAIRED episodes + if epObj.status == UNAIRED: + logger.log(u"Refusing to change status of "+curEp+" because it is UNAIRED", logger.ERROR) + continue + + if int(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): + logger.log(u"Refusing to change status of "+curEp+" to DOWNLOADED because it's not SNATCHED/DOWNLOADED", logger.ERROR) + continue + + epObj.status = int(status) + epObj.saveToDB() + + msg = "Backlog was automatically started for the following seasons of <b>"+showObj.name+"</b>:<br />" + for cur_segment in segment_list: + msg += "<li>Season "+str(cur_segment)+"</li>" + logger.log(u"Sending backlog for "+showObj.name+" season "+str(cur_segment)+" because some eps were set to wanted") + cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, cur_segment) + sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable + msg += "</ul>" + + if segment_list: + ui.notifications.message("Backlog started", msg) + + if direct: + return json.dumps({'result': 'success'}) + else: + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def setAudio(self, show=None, eps=None, audio_langs=None, direct=False): + + if show == None or eps == None or audio_langs == None: + errMsg = "You must specify a show and at least one episode" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Show not in show list") + + try: + show_loc = showObj.location #@UnusedVariable + except exceptions.ShowDirNotFoundException: + return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + + ep_obj_rename_list = [] + + for curEp in eps.split('|'): + + logger.log(u"Attempting to set audio on episode "+curEp+" to "+audio_langs, logger.DEBUG) + + epInfo = curEp.split('x') + + epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) + + epObj.audio_langs = [audio_langs] + epObj.saveToDB() + + if direct: + return json.dumps({'result': 'success'}) + else: + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def testRename(self, show=None): + + if show == None: + return _genericMessage("Error", "You must specify a show") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Show not in show list") + + try: + show_loc = showObj.location #@UnusedVariable + except exceptions.ShowDirNotFoundException: + return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + + ep_obj_rename_list = [] + + ep_obj_list = showObj.getAllEpisodes(has_location=True) + + for cur_ep_obj in ep_obj_list: + # Only want to rename if we have a location + if cur_ep_obj.location: + if cur_ep_obj.relatedEps: + # do we have one of multi-episodes in the rename list already + have_already = False + for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: + if cur_related_ep in ep_obj_rename_list: + have_already = True + break + if not have_already: + ep_obj_rename_list.append(cur_ep_obj) + + else: + ep_obj_rename_list.append(cur_ep_obj) + + if ep_obj_rename_list: + # present season DESC episode DESC on screen + ep_obj_rename_list.reverse() + + t = PageTemplate(file="testRename.tmpl") + t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.tvdbid}] + t.ep_obj_list = ep_obj_rename_list + t.show = showObj + + return _munge(t) + + @cherrypy.expose + def doRename(self, show=None, eps=None): + + if show == None or eps == None: + errMsg = "You must specify a show and at least one episode" + return _genericMessage("Error", errMsg) + + show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if show_obj == None: + errMsg = "Error", "Show not in show list" + return _genericMessage("Error", errMsg) + + try: + show_loc = show_obj.location #@UnusedVariable + except exceptions.ShowDirNotFoundException: + return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + + myDB = db.DBConnection() + + if eps == None: + redirect("/home/displayShow?show=" + show) + + for curEp in eps.split('|'): + + epInfo = curEp.split('x') + + # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database + ep_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", [show, epInfo[0], epInfo[1]]) + if not ep_result: + logger.log(u"Unable to find an episode for "+curEp+", skipping", logger.WARNING) + continue + related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE location = ? AND episode != ?", [ep_result[0]["location"], epInfo[1]]) + + root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) + for cur_related_ep in related_eps_result: + related_ep_obj = show_obj.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) + if related_ep_obj not in root_ep_obj.relatedEps: + root_ep_obj.relatedEps.append(related_ep_obj) + + root_ep_obj.rename() + + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def searchEpisode(self, show=None, season=None, episode=None): + + # retrieve the episode object and fail if we can't get one + ep_obj = _getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj) + sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) #@UndefinedVariable + + # wait until the queue item tells us whether it worked or not + while ep_queue_item.success == None: #@UndefinedVariable + time.sleep(1) + + # return the correct json value + if ep_queue_item.success: + return json.dumps({'result': statusStrings[ep_obj.status]}) + + return json.dumps({'result': 'failure'}) + +class UI: + + @cherrypy.expose + def add_message(self): + + ui.notifications.message('Test 1', 'This is test number 1') + ui.notifications.error('Test 2', 'This is test number 2') + + return "ok" + + @cherrypy.expose + def get_messages(self): + messages = {} + cur_notification_num = 1 + for cur_notification in ui.notifications.get_notifications(): + messages['notification-'+str(cur_notification_num)] = {'title': cur_notification.title, + 'message': cur_notification.message, + 'type': cur_notification.type} + cur_notification_num += 1 + + return json.dumps(messages) + + +class WebInterface: + + @cherrypy.expose + def index(self): + + redirect("/home") + + @cherrypy.expose + def showPoster(self, show=None, which=None): + + if which == 'poster': + default_image_name = 'poster.png' + else: + default_image_name = 'banner.png' + + default_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', default_image_name) + if show is None: + return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") + else: + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") + + cache_obj = image_cache.ImageCache() + + if which == 'poster': + image_file_name = cache_obj.poster_path(showObj.tvdbid) + # this is for 'banner' but also the default case + else: + image_file_name = cache_obj.banner_path(showObj.tvdbid) + + if ek.ek(os.path.isfile, image_file_name): + # use startup argument to prevent using PIL even if installed + if sickbeard.NO_RESIZE: + return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") + try: + from PIL import Image + from cStringIO import StringIO + except ImportError: # PIL isn't installed + return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") + else: + im = Image.open(image_file_name) + if im.mode == 'P': # Convert GIFs to RGB + im = im.convert('RGB') + if which == 'banner': + size = 606, 112 + elif which == 'poster': + size = 136, 200 + else: + return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") + im = im.resize(size, Image.ANTIALIAS) + imgbuffer = StringIO() + im.save(imgbuffer, 'JPEG', quality=85) + cherrypy.response.headers['Content-Type'] = 'image/jpeg' + return imgbuffer.getvalue() + else: + return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") + + @cherrypy.expose + def setComingEpsLayout(self, layout): + if layout not in ('poster', 'banner', 'list'): + layout = 'banner' + + sickbeard.COMING_EPS_LAYOUT = layout + + redirect("/comingEpisodes") + + @cherrypy.expose + def toggleComingEpsDisplayPaused(self): + + sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED + + redirect("/comingEpisodes") + + @cherrypy.expose + def setComingEpsSort(self, sort): + if sort not in ('date', 'network', 'show'): + sort = 'date' + + sickbeard.COMING_EPS_SORT = sort + + redirect("/comingEpisodes") + + @cherrypy.expose + def comingEpisodes(self, layout="None"): + + myDB = db.DBConnection() + + today = datetime.date.today().toordinal() + next_week = (datetime.date.today() + datetime.timedelta(days=7)).toordinal() + recently = (datetime.date.today() - datetime.timedelta(days=3)).toordinal() + + done_show_list = [] + qualList = Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED, IGNORED] + sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND airdate >= ? AND airdate < ? AND tv_shows.tvdb_id = tv_episodes.showid AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, next_week] + qualList) + for cur_result in sql_results: + done_show_list.append(int(cur_result["showid"])) + + more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes outer_eps, tv_shows WHERE season != 0 AND showid NOT IN ("+','.join(['?']*len(done_show_list))+") AND tv_shows.tvdb_id = outer_eps.showid AND airdate = (SELECT airdate FROM tv_episodes inner_eps WHERE 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) + sql_results += more_sql_results + + more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND tv_shows.tvdb_id = tv_episodes.showid AND airdate < ? AND airdate >= ? AND tv_episodes.status = ? AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, recently, WANTED] + qualList) + sql_results += more_sql_results + + #epList = sickbeard.comingList + + # sort by air date + sorts = { + 'date': (lambda x, y: cmp(int(x["airdate"]), int(y["airdate"]))), + 'show': (lambda a, b: cmp(a["show_name"], b["show_name"])), + 'network': (lambda a, b: cmp(a["network"], b["network"])), + } + + #epList.sort(sorts[sort]) + sql_results.sort(sorts[sickbeard.COMING_EPS_SORT]) + + t = PageTemplate(file="comingEpisodes.tmpl") + paused_item = { 'title': '', 'path': 'toggleComingEpsDisplayPaused' } + paused_item['title'] = 'Hide Paused' if sickbeard.COMING_EPS_DISPLAY_PAUSED else 'Show Paused' + t.submenu = [ + { 'title': 'Sort by:', 'path': {'Date': 'setComingEpsSort/?sort=date', + 'Show': 'setComingEpsSort/?sort=show', + 'Network': 'setComingEpsSort/?sort=network', + }}, + + { 'title': 'Layout:', 'path': {'Banner': 'setComingEpsLayout/?layout=banner', + 'Poster': 'setComingEpsLayout/?layout=poster', + 'List': 'setComingEpsLayout/?layout=list', + }}, + paused_item, + ] + + t.next_week = next_week + t.today = today + t.sql_results = sql_results + + # Allow local overriding of layout parameter + if layout and layout in ('poster', 'banner', 'list'): + t.layout = layout + else: + t.layout = sickbeard.COMING_EPS_LAYOUT + + + return _munge(t) + + manage = Manage() + + history = History() + + config = Config() + + home = Home() + + api = Api() + + browser = browser.WebFileBrowser() + + errorlogs = ErrorLogs() + + ui = UI()