1
0
mirror of https://github.com/moparisthebest/SickRage synced 2025-01-05 10:58:01 -05:00

Added anime support with anidb support.

Added fanzub anime nzb provider.
Fixed NyaaTorrents anime provider.

This is in testing phase so bugs are to be expected.
This commit is contained in:
echel0n 2014-05-25 23:29:22 -07:00
parent 3b6534ca1d
commit 9a3e7ab0a9
68 changed files with 60236 additions and 430 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,103 @@
#import sickbeard
#set global $title="Config - Anime"
#set global $header="Anime"
#set global $sbPath="../.."
#set global $topmenu="config"#
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
#if $varExists('header')
<h1 class="header">$header</h1>
#else
<h1 class="title">$title</h1>
#end if
<script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script>
<div id="config">
<div id="config-content">
<form id="configForm" action="saveAnime" method="post">
<div id="config-components">
<div id="core-component-group1" class="component-group clearfix">
<div class="component-group-desc">
<h3><a href="http://anidb.info" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/providers/anidb.gif" alt="AniDB" title="AniDB" width="16" height="16" /> AniDB</a></h3>
<p>AniDB is non-profit database of anime information that is freely open to the public</p>
</div>
<fieldset class="component-group-list">
<div class="field-pair">
<input type="checkbox" class="enabler" name="use_anidb" id="use_anidb" #if $sickbeard.USE_ANIDB then "checked=\"checked\"" else ""# />
<label class="clearfix" for="use_notifo">
<span class="component-title">Enable</span>
<span class="component-desc">Should Sick Beard use data from AniDB?</span>
</label>
</div>
<div id="content_use_anidb">
<div class="field-pair">
<label class="nocheck clearfix">
<span class="component-title">AniDB Username</span>
<input type="text" name="anidb_username" id="anidb_username" value="$sickbeard.ANIDB_USERNAME" size="35" />
</label>
<label class="nocheck clearfix">
<span class="component-title">&nbsp;</span>
<span class="component-desc">Username of your AniDB account</span>
</label>
</div>
<div class="field-pair">
<label class="nocheck clearfix">
<span class="component-title">AniDB Password</span>
<input type="password" name="anidb_password" id="anidb_password" value="$sickbeard.ANIDB_PASSWORD" size="35" />
</label>
<label class="nocheck clearfix">
<span class="component-title">&nbsp;</span>
<span class="component-desc">Password of your AniDB account</span>
</label>
</div>
<div class="field-pair">
<label class="nocheck clearfix">
<span class="component-title">AniDB MyList</span>
<input type="checkbox" name="anidb_use_mylist" id="anidb_use_mylist" #if $sickbeard.ANIDB_USE_MYLIST then "checked=\"checked\"" else ""# />
</label>
<label class="nocheck clearfix">
<span class="component-title">&nbsp;</span>
<span class="component-desc">Do you want to add the PostProcessed Episodes to the MyList ?</span>
</label>
</div>
</div>
<input type="submit" class="config_submitter" value="Save Changes" />
</fieldset>
</div><!-- /component-group //-->
<div id="core-component-group2" class="component-group clearfix">
<div class="component-group-desc">
<h3>Look and Feel</h3>
</div>
<fieldset class="component-group-list">
<div class="field-pair">
<input type="checkbox" class="enabler" name="split_home" id="split_home" #if $sickbeard.ANIME_SPLIT_HOME then "checked=\"checked\"" else ""# />
<label class="clearfix" for="use_notifo">
<span class="component-title">Split show lists</span>
<span class="component-desc">Separate anime and normal shows in groups</span>
</label>
</div>
<input type="submit" class="config_submitter" value="Save Changes" />
</fieldset>
</div><!-- /component-group //-->
<br/><input type="submit" class="config_submitter" value="Save Changes" /><br/>
</div><!-- /config-components //-->
</form>
</div></div>
<div class="clearfix"></div>
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_bottom.tmpl")

View File

@ -467,6 +467,42 @@
<br/>
</div>
<div class="field-pair">
<input type="radio" name="naming_anime" id="naming_anime" value="1" #if $sickbeard.NAMING_ANIME == 1then "checked=\"checked\"" else ""#/>
<label class="clearfix" for="naming_anime">
<span class="component-title">Add Absolute Number</span>
<span class="component-desc">Add the absolute number to the season/episode format?</span>
</label>
<label class="nocheck clearfix">
<span class="component-title">&nbsp;</span>
<span class="component-desc">Only applies to animes. (eg. S15E45 - 310 vs S15E45)</span>
</label>
</div>
<div class="field-pair">
<input type="radio" name="naming_anime" id="naming_anime_only" value="2" #if $sickbeard.NAMING_ANIME == 2 then "checked=\"checked\"" else ""#/>
<label class="clearfix" for="naming_anime_only">
<span class="component-title">Only Absolute Number</span>
<span class="component-desc">Replace season/episode format with absolute number</span>
</label>
<label class="nocheck clearfix">
<span class="component-title">&nbsp;</span>
<span class="component-desc">Only applies to animes.</span>
</label>
</div>
<div class="field-pair">
<input type="radio" name="naming_anime" id="naming_anime_none" value="3" #if $sickbeard.NAMING_ANIME == 3 then "checked=\"checked\"" else ""#/>
<label class="clearfix" for="naming_anime_none">
<span class="component-title">No Absolute Number</span>
<span class="component-desc">Dont include the absolute number</span>
</label>
<label class="nocheck clearfix">
<span class="component-title">&nbsp;</span>
<span class="component-desc">Only applies to animes.</span>
</label>
</div>
<div class="field-pair">
<input type="checkbox" id="naming_strip_year" name="naming_strip_year" #if $sickbeard.NAMING_STRIP_YEAR then "checked=\"checked\"" else ""#/>
<label class="clearfix" for="naming_strip_year">

View File

@ -22,6 +22,7 @@
<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/sceneExceptionsTooltip.js"></script>
<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/ajaxEpSubtitles.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/ajaxEpRetry.js?$sbPID"></script>
@ -31,8 +32,19 @@
<div class="align-left"><b>Change Show:</b>
<div class="navShow"><img id="prevShow" src="$sbRoot/images/prev.gif" alt="&lt;&lt;" title="Prev Show" /></div>
<select id="pickShow">
#for $curShow in $sortedShowList:
<option value="$curShow.indexerid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option>
#for $curShowList in $sortedShowLists:
#set $curShowType = $curShowList[0]
#set $curShowList = $curShowList[1]
#if len($sortedShowLists) > 1:
<optgroup label="$curShowType">
#end if
#for $curShow in $curShowList:
<option value="$curShow.indexerid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option>
#end for
#if len($sortedShowLists) > 1:
</optgroup>
#end if
#end for
</select>
<div class="navShow"><img id="nextShow" src="$sbRoot/images/next.gif" alt="&gt;&gt;" title="Next Show" /></div>
@ -158,7 +170,8 @@
<tr><td class="showLegend">Flat 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">Sports: </td><td><img src="$sbRoot/images/#if int($show.sports) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr>
<tr><td class="showLegend">Sports: </td><td><img src="$sbRoot/images/#if int($show.is_sports) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr>
<tr><td class="showLegend">Anime: </td><td><img src="$sbRoot/images/#if int($show.is_anime) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr>
<tr><td class="showLegend">DVD Order: </td><td><img src="$sbRoot/images/#if int($show.dvdorder) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr>
#if $anyQualities + $bestQualities
<tr><td class="showLegend">Archive First Match: </td><td><img src="$sbRoot/images/#if int($show.archive_firstmatch) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr>
@ -206,20 +219,36 @@
<br />
<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0">
#for $epResult in $sqlResults:
#if not $sickbeard.DISPLAY_SHOW_SPECIALS and int($epResult["season"]) == 0:
#continue
#end if
#if (epResult["season"], epResult["episode"]) in $xem_numbering:
#set ($dfltSeas, $dfltEpis, $dfltAbsolute) = $xem_numbering[(epResult["season"], epResult["episode"])]
#elif $xem_numbering and (epResult["season"], epResult["episode"]) not in $xem_numbering:
#set ($dfltSeas, $dfltEpis, $dfltAbsolute) = (0,0,0)
#else:
#set ($dfltSeas, $dfltEpis, $dfltAbsolute) = (epResult["season"], epResult["episode"], epResult["absolute_number"])
#end if
#if (epResult["season"], epResult["episode"]) in $scene_numbering:
#set ($scSeas, $scEpis, $scAbsolute) = $scene_numbering[(epResult["season"], epResult["episode"])]
#set $dfltEpNumbering = False
#else
#set ($scSeas, $scEpis, $scAbsolute) = ($dfltSeas, $dfltEpis, $dfltAbsolute)
#set $dfltEpNumbering = True
#end if
#if int($epResult["season"]) != $curSeason:
<tr><td colspan="10" style="height: 0px; padding:0; margin:0;"><a name="season-$epResult["season"]"></a></td></tr>
<tr><td colspan="11" style="height: 0px; padding:0; margin:0;"><a name="season-$epResult["season"]"></a></td></tr>
<tr class="seasonheader" id="season-$epResult["season"]" >
<td colspan="10">
<td colspan="11">
<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>Scene #</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th>#if $sickbeard.USE_SUBTITLES and $show.subtitles then "<th>Subtitles</th>" else ""#<th>Status</th><th>Search</th></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 class="nowrap">Scene #</th><th class="nowrap">Absolute #</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th>#if $sickbeard.USE_SUBTITLES and $show.subtitles then "<th>Subtitles</th>" else ""#<th>Status</th><th>Search</th></tr>
#set $curSeason = int($epResult["season"])
#end if
@ -235,34 +264,37 @@
<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 align="center">
#if int($show.air_by_date) != 1 and int($show.sports) != 1
#if (epResult["season"], epResult["episode"]) in $xem_numbering:
#set ($dfltSeas, $dfltEpis) = $xem_numbering[(epResult["season"], epResult["episode"])]
#elif $xem_numbering and (epResult["season"], epResult["episode"]) not in $xem_numbering:
#set ($dfltSeas, $dfltEpis) = (0,0)
#else:
#set ($dfltSeas, $dfltEpis) = (epResult["season"], epResult["episode"])
#end if
#if (epResult["season"], epResult["episode"]) in $scene_numbering:
#set ($scSeas, $scEpis) = $scene_numbering[(epResult["season"], epResult["episode"])]
#set $dfltEpNumbering = False
#else
#set ($scSeas, $scEpis) = ($dfltSeas, $dfltEpis)
#set $dfltEpNumbering = True
#end if
<input type="text" placeholder="<%=str(dfltSeas) + 'x' + str(dfltEpis)%>" size="6" maxlength="8"
class="sceneSeasonXEpisode" data-for-season="$epResult["season"]" data-for-episode="$epResult["episode"]"
id="sceneSeasonXEpisode_$show.indexerid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>"
title="Change the value here if scene numbering differs from the indexer episode numbering"
#if $dfltEpNumbering:
value=""
#else
value="<%=str(scSeas) + 'x' + str(scEpis)%>"
#end if
style="padding: 0; text-align: center; max-width: 60px;"
/>
#if not $show.air_by_date and not $show.is_sports
<input type="text" placeholder="<%=str(dfltSeas) + 'x' + str(dfltEpis)%>" size="6" maxlength="8"
class="sceneSeasonXEpisode" data-for-season="$epResult["season"]" data-for-episode="$epResult["episode"]"
id="sceneSeasonXEpisode_$show.indexerid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>"
title="Change the value here if scene numbering differs from the indexer episode numbering"
#if $dfltEpNumbering:
value=""
#else
value="<%=str(scSeas) + 'x' + str(scEpis)%>"
#end if
style="padding: 0; text-align: center; max-width: 60px;"
/>
#else
N/A
-
#end if
</td>
<td align="center">
#if not $show.air_by_date and not $show.is_sports
<input type="text" placeholder="<%=str(dfltAbsolute)%>" size="6" maxlength="8"
class="sceneAbsolute" data-for-season="$epResult["season"]" data-for-episode="$epResult["episode"]"
id="sceneAbsolute_$show.indexerid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>"
title="Change the value here if scene absolute numbering differs from the indexer absolute numbering"
#if $dfltEpNumbering:
value=""
#else
value="<%=str(scAbsolute)%>"
#end if
style="padding: 0; text-align: center; max-width: 60px;"
/>
#else
-
#end if
</td>
<td class="title">

View File

@ -120,6 +120,10 @@ This <b>DOES NOT</b> allow SickRage to download non-english TV episodes!<br />
<input type="checkbox" name="sports" #if $show.sports == 1 then "checked=\"checked\"" else ""# /><br />
(check this if the show is a sporting or MMA event)
<br /><br />
<b>Anime: </b>
<input type="checkbox" name="anime" #if $show.is_anime then "CHECKED" else ""#><br />
(check this if the show is released as Show.265 rather than Show.S02E03, this show is an anime)
<br /><br />
<b>DVD Order: </b>
<input type="checkbox" name="dvdorder" #if $show.dvdorder == 1 then "checked=\"checked\"" else ""# /><br/>
(check this if you wish to use the DVD order instead of the Airing order)

View File

@ -33,6 +33,14 @@
</label>
</div>
<div class="field-pair alt">
<input type="checkbox" name="anime" id="anime" #if $sickbeard.ANIME_DEFAULT then "checked=\"checked\"" else ""# />
<label for="anime" class="clearfix">
<span class="component-title">Anime</span>
<span class="component-desc">Is this show an Anime?</span>
</label>
</div>
#set $qualities = $Quality.splitQuality($sickbeard.QUALITY_DEFAULT)
#set global $anyQualities = $qualities[0]
#set global $bestQualities = $qualities[1]

View File

@ -272,6 +272,7 @@ a > i.icon-question-sign { background-image: url("$sbRoot/images/glyphicons-half
<li><a href="$sbRoot/config/subtitles/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Subtitles Settings</a></li>
<li><a href="$sbRoot/config/postProcessing/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Post Processing</a></li>
<li><a href="$sbRoot/config/notifications/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Notifications</a></li>
<li><a href="$sbRoot/config/anime/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Anime</a></li>
</ul>
</li>
<li id="donate"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YCTA5TEN2JE2J" rel="noreferrer" onclick="window.open('${sickbeard.ANON_REDIRECT}' + this.href); return false;"><img src="$sbRoot/images/paypal/btn_donate_LG.gif" alt="[donate]" /></a></li>

View File

@ -220,7 +220,7 @@ $(document).ready(function () {
$('#naming_custom_sports').change(function () {
setup_sports_naming();
});
$('#naming_multi_ep').change(fill_examples);
$('#naming_pattern').focusout(fill_examples);
$('#naming_pattern').keyup(function () {

View File

@ -0,0 +1,39 @@
$(function () {
$('.showTitle a').each(function () {
match = $(this).parent().attr("id").match(/^scene_exception_(\d+)$/);
$(this).qtip({
content: {
text: 'Loading...',
ajax: {
url: $("#sbRoot").val() + '/home/sceneExceptions',
type: 'GET',
data: {
show: match[1]
},
success: function (data, status) {
this.set('content.text', data);
}
}
},
show: {
solo: true
},
position: {
viewport: $(window),
my: 'top center',
at: 'bottom center',
adjust: {
y: 3,
x: 0
}
},
style: {
tip: {
corner: true,
method: 'polygon'
},
classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-sb'
}
});
});
});

796
lib/adba/__init__.py Normal file
View File

@ -0,0 +1,796 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
import threading
from time import time, sleep, strftime, localtime
from types import *
from aniDBlink import AniDBLink
from aniDBcommands import *
from aniDBerrors import *
from aniDBAbstracter import Anime, Episode
version = 100
class Connection(threading.Thread):
def __init__(self, clientname='adba', server='api.anidb.info', port=9000, myport=9876, user=None, password=None, session=None, log=False, logPrivate=False, keepAlive=False):
super(Connection, self).__init__()
# setting the log function
self.logPrivate = logPrivate
if type(log) in (FunctionType, MethodType):# if we get a function or a method use that.
self.log = log
self.logPrivate = True # true means sensitive data will not be NOT be logged ... yeah i know oO
elif log:# if it something else (like True) use the own print_log
self.log = self.print_log
else:# dont log at all
self.log = self.print_log_dummy
self.link = AniDBLink(server, port, myport, self.log, logPrivate=self.logPrivate)
self.link.session = session
self.clientname = clientname
self.clientver = version
# from original lib
self.mode = 1 #mode: 0=queue,1=unlock,2=callback
# to lock other threads out
self.lock = threading.RLock()
# thread keep alive stuff
self.keepAlive = keepAlive
self.setDaemon(True)
self.lastKeepAliveCheck = 0
self.lastAuth = 0
self._username = password
self._password = user
self._iamALIVE = False
self.counter = 0
self.counterAge = 0
def print_log(self, data):
print(strftime("%Y-%m-%d %H:%M:%S", localtime(time())) + ": " + str(data))
def print_log_dummy(self, data):
pass
def stop(self):
self.logout(cutConnection=True)
def cut(self):
self.link.stop()
def handle_response(self, response):
if response.rescode in ('501', '506') and response.req.command != 'AUTH':
self.log("seams like the last command got a not authed error back tring to reconnect now")
if self._reAuthenticate():
response.req.resp = None
response = self.handle(response.req, response.req.callback)
def handle(self, command, callback):
self.lock.acquire()
if self.counterAge < (time() - 120): # the last request was older then 2 min reset delay and counter
self.counter = 0
self.link.delay = 2
else: # something happend in the last 120 seconds
if self.counter < 5:
self.link.delay = 2 # short term "A Client MUST NOT send more than 0.5 packets per second (that's one packet every two seconds, not two packets a second!)"
elif self.counter >= 5:
self.link.delay = 6 # long term "A Client MUST NOT send more than one packet every four seconds over an extended amount of time."
if command.command not in ('AUTH', 'PING', 'ENCRYPT'):
self.counterAge = time()
self.counter += 1
if self.keepAlive:
self.authed()
def callback_wrapper(resp):
self.handle_response(resp)
if callback:
callback(resp)
self.log("handling(" + str(self.counter) + "-" + str(self.link.delay) + ") command " + str(command.command))
#make live request
command.authorize(self.mode, self.link.new_tag(), self.link.session, callback_wrapper)
self.link.request(command)
#handle mode 1 (wait for response)
if self.mode == 1:
command.wait_response()
try:
command.resp
except:
self.lock.release()
if self.link.banned:
raise AniDBBannedError("User is banned")
else:
raise AniDBCommandTimeoutError("Command has timed out")
self.handle_response(command.resp)
self.lock.release()
return command.resp
else:
self.lock.release()
def authed(self, reAuthenticate=False):
self.lock.acquire()
authed = (self.link.session != None)
if not authed and (reAuthenticate or self.keepAlive):
self._reAuthenticate()
authed = (self.link.session != None)
self.lock.release()
return authed
def _reAuthenticate(self):
if self._username and self._password:
self.log("auto re authenticating !")
resp = self.auth(self._username, self._password)
if resp.rescode not in ('500'):
return True
else:
return False
def _keep_alive(self):
self.lastKeepAliveCheck = time()
self.log("auto check !")
# check every 30 minutes if the session is still valid
# if not reauthenticate
if self.lastAuth and time() - self.lastAuth > 1800:
self.log("auto uptime !")
self.uptime() # this will update the self.link.session and will refresh the session if it is still alive
if self.authed(): # if we are authed we set the time
self.lastAuth = time()
else: # if we aren't authed and we have the user and pw then reauthenticate
self._reAuthenticate()
# issue a ping every 20 minutes after the last package
# this ensures the connection will be kept alive
if self.link.lastpacket and time() - self.link.lastpacket > 1200:
self.log("auto ping !")
self.ping()
def run(self):
while self.keepAlive:
self._keep_alive()
sleep(120)
def auth(self, username, password, nat=None, mtu=None, callback=None):
"""
Login to AniDB UDP API
parameters:
username - your anidb username
password - your anidb password
nat - if this is 1, response will have "address" in attributes with your "ip:port" (default:0)
mtu - maximum transmission unit (max packet size) (default: 1400)
"""
self.log("ok1")
if self.keepAlive:
self.log("ok2")
self._username = username
self._password = password
if self.is_alive() == False:
self.log("You wanted to keep this thing alive!")
if self._iamALIVE == False:
self.log("Starting thread now...")
self.start()
self._iamALIVE = True
else:
self.log("not starting thread seams like it is already running. this must be a _reAuthenticate")
self.lastAuth = time()
return self.handle(AuthCommand(username, password, 3, self.clientname, self.clientver, nat, 1, 'utf8', mtu), callback)
def logout(self, cutConnection=False, callback=None):
"""
Log out from AniDB UDP API
"""
result = self.handle(LogoutCommand(), callback)
if(cutConnection):
self.cut()
return result
def push(self, notify, msg, buddy=None, callback=None):
"""
Subscribe/unsubscribe to/from notifications
parameters:
notify - Notifications about files added?
msg - Notifications about message added?
buddy - Notifications about buddy events?
structure of parameters:
notify msg [buddy]
"""
return self.handle(PushCommand(notify, msg, buddy), callback)
def pushack(self, nid, callback=None):
"""
Acknowledge notification (do this when you get 271-274)
parameters:
nid - Notification packet id
structure of parameters:
nid
"""
return self.handle(PushAckCommand(nid), callback)
def notifyadd(self, aid=None, gid=None, type=None, priority=None, callback=None):
"""
Add a notification
parameters:
aid - Anime id
gid - Group id
type - Type of notification: type=> 0=all, 1=new, 2=group, 3=complete
priority - low = 0, medium = 1, high = 2 (unconfirmed)
structure of parameters:
[aid={int}|gid={int}]&type={int}&priority={int}
"""
return self.handle(NotifyAddCommand(aid, gid, type, priority), callback)
def notify(self, buddy=None, callback=None):
"""
Get number of pending notifications and messages
parameters:
buddy - Also display number of online buddies
structure of parameters:
[buddy]
"""
return self.handle(NotifyCommand(buddy), callback)
def notifylist(self, callback=None):
"""
List all pending notifications/messages
"""
return self.handle(NotifyListCommand(), callback)
def notifyget(self, type, id, callback=None):
"""
Get notification/message
parameters:
type - (M=message, N=notification)
id - message/notification id
structure of parameters:
type id
"""
return self.handle(NotifyGetCommand(type, id), callback)
def notifyack(self, type, id, callback=None):
"""
Mark message read or clear a notification
parameters:
type - (M=message, N=notification)
id - message/notification id
structure of parameters:
type id
"""
return self.handle(NotifyAckCommand(type, id), callback)
def buddyadd(self, uid=None, uname=None, callback=None):
"""
Add a user to your buddy list
parameters:
uid - user id
uname - name of the user
structure of parameters:
(uid|uname)
"""
return self.handle(BuddyAddCommand(uid, uname), callback)
def buddydel(self, uid, callback=None):
"""
Remove a user from your buddy list
parameters:
uid - user id
structure of parameters:
uid
"""
return self.handle(BuddyDelCommand(uid), callback)
def buddyaccept(self, uid, callback=None):
"""
Accept user as buddy
parameters:
uid - user id
structure of parameters:
uid
"""
return self.handle(BuddyAcceptCommand(uid), callback)
def buddydeny(self, uid, callback=None):
"""
Deny user as buddy
parameters:
uid - user id
structure of parameters:
uid
"""
return self.handle(BuddyDenyCommand(uid), callback)
def buddylist(self, startat, callback=None):
"""
Retrieve your buddy list
parameters:
startat - number of buddy to start listing from
structure of parameters:
startat
"""
return self.handle(BuddyListCommand(startat), callback)
def buddystate(self, startat, callback=None):
"""
Retrieve buddy states
parameters:
startat - number of buddy to start listing from
structure of parameters:
startat
"""
return self.handle(BuddyStateCommand(startat), callback)
def anime(self, aid=None, aname=None, amask= -1, callback=None):
"""
Get information about an anime
parameters:
aid - anime id
aname - name of the anime
amask - a bitfield describing what information you want about the anime
structure of parameters:
(aid|aname) [amask]
structure of amask:
"""
return self.handle(AnimeCommand(aid, aname, amask), callback)
def episode(self, eid=None, aid=None, aname=None, epno=None, callback=None):
"""
Get information about an episode
parameters:
eid - episode id
aid - anime id
aname - name of the anime
epno - number of the episode
structure of parameters:
eid
(aid|aname) epno
"""
return self.handle(EpisodeCommand(eid, aid, aname, epno), callback)
def file(self, fid=None, size=None, ed2k=None, aid=None, aname=None, gid=None, gname=None, epno=None, fmask= -1, amask=0, callback=None):
"""
Get information about a file
parameters:
fid - file id
size - size of the file
ed2k - ed2k-hash of the file
aid - anime id
aname - name of the anime
gid - group id
gname - name of the group
epno - number of the episode
fmask - a bitfield describing what information you want about the file
amask - a bitfield describing what information you want about the anime
structure of parameters:
fid [fmask] [amask]
size ed2k [fmask] [amask]
(aid|aname) (gid|gname) epno [fmask] [amask]
structure of fmask:
bit key description
0 - -
1 aid aid
2 eid eid
3 gid gid
4 lid lid
5 - -
6 - -
7 - -
8 state state
9 size size
10 ed2k ed2k
11 md5 md5
12 sha1 sha1
13 crc32 crc32
14 - -
15 - -
16 dublang dub language
17 sublang sub language
18 quality quality
19 source source
20 audiocodec audio codec
21 audiobitrate audio bitrate
22 videocodec video codec
23 videobitrate video bitrate
24 resolution video resolution
25 filetype file type (extension)
26 length length in seconds
27 description description
28 - -
29 - -
30 filename anidb file name
31 - -
structure of amask:
bit key description
0 gname group name
1 gshortname group short name
2 - -
3 - -
4 - -
5 - -
6 - -
7 - -
8 epno epno
9 epname ep english name
10 epromaji ep romaji name
11 epkanji ep kanji name
12 - -
13 - -
14 - -
15 - -
16 totaleps anime total episodes
17 lastep last episode nr (highest, not special)
18 year year
19 type type
20 romaji romaji name
21 kanji kanji name
22 name english name
23 othername other name
24 shortnames short name list
25 synonyms synonym list
26 categories category list
27 relatedaids related aid list
28 producernames producer name list
29 producerids producer id list
30 - -
31 - -
"""
return self.handle(FileCommand(fid, size, ed2k, aid, aname, gid, gname, epno, fmask, amask), callback)
def group(self, gid=None, gname=None, callback=None):
"""
Get information about a group
parameters:
gid - group id
gname - name of the group
structure of parameters:
(gid|gname)
"""
return self.handle(GroupCommand(gid, gname), callback)
def groupstatus(self, aid=None, state=None, callback=None):
"""
Returns a list of group names and ranges of episodes released by the group for a given anime.
parameters:
aid - anime id
state - If state is not supplied, groups with a completion state of 'ongoing', 'finished', or 'complete' are returned
state values:
1 -> ongoing
2 -> stalled
3 -> complete
4 -> dropped
5 -> finished
6 -> specials only
"""
return self.handle(GroupstatusCommand(aid, state), callback)
def producer(self, pid=None, pname=None, callback=None):
"""
Get information about a producer
parameters:
pid - producer id
pname - name of the producer
structure of parameters:
(pid|pname)
"""
return self.handle(ProducerCommand(pid, pname), callback)
def mylist(self, lid=None, fid=None, size=None, ed2k=None, aid=None, aname=None, gid=None, gname=None, epno=None, callback=None):
"""
Get information about your mylist
parameters:
lid - mylist id
fid - file id
size - size of the file
ed2k - ed2k-hash of the file
aid - anime id
aname - name of the anime
gid - group id
gname - name of the group
epno - number of the episode
structure of parameters:
lid
fid
size ed2k
(aid|aname) (gid|gname) epno
"""
return self.handle(MyListCommand(lid, fid, size, ed2k, aid, aname, gid, gname, epno), callback)
def mylistadd(self, lid=None, fid=None, size=None, ed2k=None, aid=None, aname=None, gid=None, gname=None, epno=None, edit=None, state=None, viewed=None, source=None, storage=None, other=None, callback=None):
"""
Add/Edit information to/in your mylist
parameters:
lid - mylist id
fid - file id
size - size of the file
ed2k - ed2k-hash of the file
aid - anime id
aname - name of the anime
gid - group id
gname - name of the group
epno - number of the episode
edit - whether to add to mylist or edit an existing entry (0=add,1=edit)
state - the location of the file
viewed - whether you have watched the file (0=unwatched,1=watched)
source - where you got the file (bittorrent,dc++,ed2k,...)
storage - for example the title of the cd you have this on
other - other data regarding this file
structure of parameters:
lid edit=1 [state viewed source storage other]
fid [state viewed source storage other] [edit]
size ed2k [state viewed source storage other] [edit]
(aid|aname) (gid|gname) epno [state viewed source storage other]
(aid|aname) edit=1 [(gid|gname) epno] [state viewed source storage other]
structure of state:
value meaning
0 unknown - state is unknown or the user doesn't want to provide this information
1 on hdd - the file is stored on hdd
2 on cd - the file is stored on cd
3 deleted - the file has been deleted or is not available for other reasons (i.e. reencoded)
structure of epno:
value meaning
x target episode x
0 target all episodes
-x target all episodes upto x
"""
return self.handle(MyListAddCommand(lid, fid, size, ed2k, aid, aname, gid, gname, epno, edit, state, viewed, source, storage, other), callback)
def mylistdel(self, lid=None, fid=None, aid=None, aname=None, gid=None, gname=None, epno=None, callback=None):
"""
Delete information from your mylist
parameters:
lid - mylist id
fid - file id
size - size of the file
ed2k - ed2k-hash of the file
aid - anime id
aname - name of the anime
gid - group id
gname - name of the group
epno - number of the episode
structure of parameters:
lid
fid
(aid|aname) (gid|gname) epno
"""
return self.handle(MyListCommand(lid, fid, aid, aname, gid, gname, epno), callback)
def myliststats(self, callback=None):
"""
Get summary information of your mylist
"""
return self.handle(MyListStatsCommand(), callback)
def vote(self, type, id=None, name=None, value=None, epno=None, callback=None):
"""
Rate an anime/episode/group
parameters:
type - type of the vote
id - anime/group id
name - name of the anime/group
value - the vote
epno - number of the episode
structure of parameters:
type (id|name) [value] [epno]
structure of type:
value meaning
1 rate an anime (episode if you also specify epno)
2 rate an anime temporarily (you haven't watched it all)
3 rate a group
structure of value:
value meaning
-x revoke vote
0 get old vote
100-1000 give vote
"""
return self.handle(VoteCommand(type, id, name, value, epno), callback)
def randomanime(self, type, callback=None):
"""
Get information of random anime
parameters:
type - where to take the random anime
structure of parameters:
type
structure of type:
value meaning
0 db
1 watched
2 unwatched
3 mylist
"""
return self.handle(RandomAnimeCommand(type), callback)
def ping(self, callback=None):
"""
Test connectivity to AniDB UDP API
"""
return self.handle(PingCommand(), callback)
def encrypt(self, user, apipassword, type=None, callback=None):
"""
Encrypt all future traffic
parameters:
user - your username
apipassword - your api password
type - type of encoding (1=128bit AES)
structure of parameters:
user [type]
"""
return self.handle(EncryptCommand(user, apipassword, type), callback)
def encoding(self, name, callback=None):
"""
Change encoding used in messages
parameters:
name - name of the encoding
structure of parameters:
name
comments:
DO NOT USE THIS!
utf8 is the only encoding which will support all the text in anidb responses
the responses have japanese, russian, french and probably other alphabets as well
even if you can't display utf-8 locally, don't change the server-client -connections encoding
rather, make python convert the encoding when you DISPLAY the text
it's better that way, let it go as utf8 to databases etc. because then you've the real data stored
"""
raise AniDBStupidUserError, "pylibanidb sets the encoding to utf8 as default and it's stupid to use any other encoding. you WILL lose some data if you use other encodings, and now you've been warned. you will need to modify the code yourself if you want to do something as stupid as changing the encoding"
return self.handle(EncodingCommand(name), callback)
def sendmsg(self, to, title, body, callback=None):
"""
Send message
parameters:
to - name of the user you want as the recipient
title - title of the message
body - the message
structure of parameters:
to title body
"""
return self.handle(SendMsgCommand(to, title, body), callback)
def user(self, user, callback=None):
"""
Retrieve user id
parameters:
user - username of the user
structure of parameters:
user
"""
return self.handle(UserCommand(user), callback)
def uptime(self, callback=None):
"""
Retrieve server uptime
"""
return self.handle(UptimeCommand(), callback)
def version(self, callback=None):
"""
Retrieve server version
"""
return self.handle(VersionCommand(), callback)

293
lib/adba/aniDBAbstracter.py Normal file
View File

@ -0,0 +1,293 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
from time import time, sleep
import aniDBfileInfo as fileInfo
import xml.etree.cElementTree as etree
import os, re, string
from aniDBmaper import AniDBMaper
from aniDBtvDBmaper import TvDBMap
from aniDBerrors import *
class aniDBabstractObject(object):
def __init__(self, aniDB, load=False):
self.laoded = False
self.set_connection(aniDB)
if load:
self.load_data()
def set_connection(self, aniDB):
self.aniDB = aniDB
if self.aniDB:
self.log = self.aniDB.log
else:
self.log = self._fake_log()
def _fake_log(self, x=None):
pass
def _fill(self, dataline):
for key in dataline:
try:
tmpList = dataline[key].split("'")
if len(tmpList) > 1:
newList = []
for i in tmpList:
try:
newList.append(int(i))
except:
newList.append(unicode(i, "utf-8"))
self.__dict__[key] = newList
continue
except:
pass
try:
self.__dict__[key] = int(dataline[key])
except:
self.__dict__[key] = unicode(dataline[key], "utf-8")
key = property(lambda x: dataline[key])
def __getattr__(self, name):
try:
return object.__getattribute__(self, name)
except:
return None
def _build_names(self):
names = []
names = self._easy_extend(names, self.english_name)
names = self._easy_extend(names, self.short_name_list)
names = self._easy_extend(names, self.synonym_list)
names = self._easy_extend(names, self.other_name)
self.allNames = names
def _easy_extend(self, initialList, item):
if item:
if isinstance(item, list):
initialList.extend(item)
elif isinstance(item, basestring):
initialList.append(item)
return initialList
def load_data(self):
return False
def add_notification(self):
"""
type - Type of notification: type=> 0=all, 1=new, 2=group, 3=complete
priority - low = 0, medium = 1, high = 2 (unconfirmed)
"""
if(self.aid):
self.aniDB.notifyadd(aid=self.aid, type=1, priority=1)
class Anime(aniDBabstractObject):
def __init__(self, aniDB, name=None, aid=None, tvdbid=None, tvrageid=None, paramsA=None, autoCorrectName=False, load=False):
self.maper = AniDBMaper()
self.tvDBMap = TvDBMap()
self.allAnimeXML = None
self.name = name
self.aid = aid
self.tvdb_id = tvdbid
if self.tvdb_id and not self.aid:
self.aid = self.tvDBMap.get_anidb_for_tvdb(self.tvdb_id)
if not (self.name or self.aid):
raise AniDBIncorrectParameterError("No aid or name available")
if not self.aid:
self.aid = self._get_aid_from_xml(self.name)
if not self.name or autoCorrectName:
self.name = self._get_name_from_xml(self.aid)
if not (self.name or self.aid):
raise ValueError
if not self.tvdb_id:
self.tvdb_id = self.tvDBMap.get_tvdb_for_anidb(self.aid)
if not paramsA:
self.bitCode = "b2f0e0fc000000"
self.params = self.maper.getAnimeCodesA(self.bitCode)
else:
self.paramsA = paramsA
self.bitCode = self.maper.getAnimeBitsA(self.paramsA)
super(Anime, self).__init__(aniDB, load)
def load_data(self):
"""load the data from anidb"""
if not (self.name or self.aid):
raise ValueError
self.rawData = self.aniDB.anime(aid=self.aid, aname=self.name, amask=self.bitCode)
if self.rawData.datalines:
self._fill(self.rawData.datalines[0])
self._builPreSequal()
self.laoded = True
def get_groups(self):
if not self.aid:
return []
self.rawData = self.aniDB.groupstatus(aid=self.aid)
self.release_groups = []
for line in self.rawData.datalines:
self.release_groups.append({"name":unicode(line["name"], "utf-8"),
"rating":line["rating"],
"range":line["episode_range"]
})
return self.release_groups
#TODO: refactor and use the new functions in anidbFileinfo
def _get_aid_from_xml(self, name):
if not self.allAnimeXML:
self.allAnimeXML = self._read_animetitels_xml()
regex = re.compile('( \(\d{4}\))|[%s]' % re.escape(string.punctuation)) # remove any punctuation and e.g. ' (2011)'
#regex = re.compile('[%s]' % re.escape(string.punctuation)) # remove any punctuation and e.g. ' (2011)'
name = regex.sub('', name.lower())
lastAid = 0
for element in self.allAnimeXML.getiterator():
if element.get("aid", False):
lastAid = int(element.get("aid"))
if element.text:
testname = regex.sub('', element.text.lower())
if testname == name:
return lastAid
return 0
#TODO: refactor and use the new functions in anidbFileinfo
def _get_name_from_xml(self, aid, onlyMain=True):
if not self.allAnimeXML:
self.allAnimeXML = self._read_animetitels_xml()
for anime in self.allAnimeXML.findall("anime"):
if int(anime.get("aid", False)) == aid:
for title in anime.getiterator():
currentLang = title.get("{http://www.w3.org/XML/1998/namespace}lang", False)
currentType = title.get("type", False)
if (currentLang == "en" and not onlyMain) or currentType == "main":
return title.text
return ""
def _read_animetitels_xml(self, path=None):
if not path:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "animetitles.xml")
f = open(path, "r")
allAnimeXML = etree.ElementTree(file=f)
return allAnimeXML
def _builPreSequal(self):
if self.related_aid_list and self.related_aid_type:
try:
for i in range(len(self.related_aid_list)):
if self.related_aid_type[i] == 2:
self.__dict__["prequal"] = self.related_aid_list[i]
elif self.related_aid_type[i] == 1:
self.__dict__["sequal"] = self.related_aid_list[i]
except:
if self.related_aid_type == 2:
self.__dict__["prequal"] = self.related_aid_list
elif self.str_related_aid_type == 1:
self.__dict__["sequal"] = self.related_aid_list
class Episode(aniDBabstractObject):
def __init__(self, aniDB, number=None, epid=None, filePath=None, fid=None, epno=None, paramsA=None, paramsF=None, load=False, calculate=False):
if not aniDB and not number and not epid and not file and not fid:
return None
self.maper = AniDBMaper()
self.epid = epid
self.filePath = filePath
self.fid = fid
self.epno = epno
if calculate:
(self.ed2k, self.size) = self._calculate_file_stuff(self.filePath)
if not paramsA:
self.bitCodeA = "C000F0C0"
self.paramsA = self.maper.getFileCodesA(self.bitCodeA)
else:
self.paramsA = paramsA
self.bitCodeA = self.maper.getFileBitsA(self.paramsA)
if not paramsF:
self.bitCodeF = "7FF8FEF8"
self.paramsF = self.maper.getFileCodesF(self.bitCodeF)
else:
self.paramsF = paramsF
self.bitCodeF = self.maper.getFileBitsF(self.paramsF)
super(Episode, self).__init__(aniDB, load)
def load_data(self):
"""load the data from anidb"""
if self.filePath and not (self.ed2k or self.size):
(self.ed2k, self.size) = self._calculate_file_stuff(self.filePath)
self.rawData = self.aniDB.file(fid=self.fid, size=self.size, ed2k=self.ed2k, aid=self.aid, aname=None, gid=None, gname=None, epno=self.epno, fmask=self.bitCodeF, amask=self.bitCodeA)
self._fill(self.rawData.datalines[0])
self._build_names()
self.laoded = True
def add_to_mylist(self, status=None):
"""
status:
0 unknown - state is unknown or the user doesn't want to provide this information (default)
1 on hdd - the file is stored on hdd
2 on cd - the file is stored on cd
3 deleted - the file has been deleted or is not available for other reasons (i.e. reencoded)
"""
if self.filePath and not (self.ed2k or self.size):
(self.ed2k, self.size) = self._calculate_file_stuff(self.filePath)
try:
self.aniDB.mylistadd(size=self.size, ed2k=self.ed2k, state=status)
except Exception, e :
self.log(u"exception msg: " + str(e))
else:
# TODO: add the name or something
self.log(u"Added the episode to anidb")
def _calculate_file_stuff(self, filePath):
if not filePath:
return (None, None)
self.log("Calculating the ed2k. Please wait...")
ed2k = fileInfo.get_file_hash(filePath)
size = fileInfo.get_file_size(filePath)
return (ed2k, size)

388
lib/adba/aniDBcommands.py Normal file
View File

@ -0,0 +1,388 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
from threading import Lock
from aniDBresponses import *
from aniDBerrors import *
class Command:
queue={None:None}
def __init__(self,command,**parameters):
self.command=command
self.parameters=parameters
self.raw=self.flatten(command,parameters)
self.mode=None
self.callback=None
self.waiter=Lock()
self.waiter.acquire()
def __repr__(self):
return "Command(%s,%s) %s\n%s\n"%(repr(self.tag),repr(self.command),repr(self.parameters),self.raw_data())
def authorize(self,mode,tag,session,callback):
self.mode=mode
self.callback=callback
self.tag=tag
self.session=session
self.parameters['tag']=tag
self.parameters['s']=session
def handle(self,resp):
self.resp=resp
if self.mode==1:
self.waiter.release()
elif self.mode==2:
self.callback(resp)
def wait_response(self):
self.waiter.acquire()
def flatten(self,command,parameters):
tmp=[]
for key,value in parameters.iteritems():
if value==None:
continue
tmp.append("%s=%s"%(self.escape(key),self.escape(value)))
return ' '.join([command,'&'.join(tmp)])
def escape(self,data):
return str(data).replace('&','&amp;')
def raw_data(self):
self.raw=self.flatten(self.command,self.parameters)
return self.raw
def cached(self,interface,database):
return None
def cache(self,interface,database):
pass
#first run
class AuthCommand(Command):
def __init__(self,username,password,protover,client,clientver,nat=None,comp=None,enc=None,mtu=None):
parameters={'user':username,'pass':password,'protover':protover,'client':client,'clientver':clientver,'nat':nat,'comp':comp,'enc':enc,'mtu':mtu}
Command.__init__(self,'AUTH',**parameters)
class LogoutCommand(Command):
def __init__(self):
Command.__init__(self,'LOGOUT')
#third run (at the same time as second)
class PushCommand(Command):
def __init__(self,notify,msg,buddy=None):
parameters={'notify':notify,'msg':msg,'buddy':buddy}
Command.__init__(self,'PUSH',**parameters)
class PushAckCommand(Command):
def __init__(self,nid):
parameters={'nid':nid}
Command.__init__(self,'PUSHACK',**parameters)
class NotifyAddCommand(Command):
def __init__(self,aid=None,gid=None,type=None,priority=None):
if not (aid or gid) or (aid and gid):
raise AniDBIncorrectParameterError,"You must provide aid OR gid for NOTIFICATIONADD command"
parameters={'aid':aid,"gid":gid,"type":type,"priority":priority}
Command.__init__(self,'NOTIFICATIONADD',**parameters)
class NotifyCommand(Command):
def __init__(self,buddy=None):
parameters={'buddy':buddy}
Command.__init__(self,'NOTIFY',**parameters)
class NotifyListCommand(Command):
def __init__(self):
Command.__init__(self,'NOTIFYLIST')
class NotifyGetCommand(Command):
def __init__(self,type,id):
parameters={'type':type,'id':id}
Command.__init__(self,'NOTIFYGET',**parameters)
class NotifyAckCommand(Command):
def __init__(self,type,id):
parameters={'type':type,'id':id}
Command.__init__(self,'NOTIFYACK',**parameters)
class BuddyAddCommand(Command):
def __init__(self,uid=None,uname=None):
if not (uid or uname) or (uid and uname):
raise AniDBIncorrectParameterError,"You must provide <u(id|name)> for BUDDYADD command"
parameters={'uid':uid,'uname':uname.lower()}
Command.__init__(self,'BUDDYADD',**parameters)
class BuddyDelCommand(Command):
def __init__(self,uid):
parameters={'uid':uid}
Command.__init__(self,'BUDDYDEL',**parameters)
class BuddyAcceptCommand(Command):
def __init__(self,uid):
parameters={'uid':uid}
Command.__init__(self,'BUDDYACCEPT',**parameters)
class BuddyDenyCommand(Command):
def __init__(self,uid):
parameters={'uid':uid}
Command.__init__(self,'BUDDYDENY',**parameters)
class BuddyListCommand(Command):
def __init__(self,startat):
parameters={'startat':startat}
Command.__init__(self,'BUDDYLIST',**parameters)
class BuddyStateCommand(Command):
def __init__(self,startat):
parameters={'startat':startat}
Command.__init__(self,'BUDDYSTATE',**parameters)
#first run
class AnimeCommand(Command):
def __init__(self,aid=None,aname=None,amask=None):
if not (aid or aname):
raise AniDBIncorrectParameterError,"You must provide <a(id|name)> for ANIME command"
parameters={'aid':aid,'aname':aname,'amask':amask}
Command.__init__(self,'ANIME',**parameters)
class EpisodeCommand(Command):
def __init__(self,eid=None,aid=None,aname=None,epno=None):
if not (eid or ((aname or aid) and epno)) or (aname and aid) or (eid and (aname or aid or epno)):
raise AniDBIncorrectParameterError,"You must provide <eid XOR a(id|name)+epno> for EPISODE command"
parameters={'eid':eid,'aid':aid,'aname':aname,'epno':epno}
Command.__init__(self,'EPISODE',**parameters)
class FileCommand(Command):
def __init__(self,fid=None,size=None,ed2k=None,aid=None,aname=None,gid=None,gname=None,epno=None,fmask=None,amask=None):
if not (fid or (size and ed2k) or ((aid or aname) and (gid or gname) and epno)) or (fid and (size or ed2k or aid or aname or gid or gname or epno)) or ((size and ed2k) and (fid or aid or aname or gid or gname or epno)) or (((aid or aname) and (gid or gname) and epno) and (fid or size or ed2k)) or (aid and aname) or (gid and gname):
raise AniDBIncorrectParameterError,"You must provide <fid XOR size+ed2k XOR a(id|name)+g(id|name)+epno> for FILE command"
parameters={'fid':fid,'size':size,'ed2k':ed2k,'aid':aid,'aname':aname,'gid':gid,'gname':gname,'epno':epno,'fmask':fmask,'amask':amask}
Command.__init__(self,'FILE',**parameters)
class GroupCommand(Command):
def __init__(self,gid=None,gname=None):
if not (gid or gname) or (gid and gname):
raise AniDBIncorrectParameterError,"You must provide <g(id|name)> for GROUP command"
parameters={'gid':gid,'gname':gname}
Command.__init__(self,'GROUP',**parameters)
class GroupstatusCommand(Command):
def __init__(self,aid=None,status=None):
if not aid:
raise AniDBIncorrectParameterError,"You must provide aid for GROUPSTATUS command"
parameters={'aid':aid,'status':status}
Command.__init__(self,'GROUPSTATUS',**parameters)
class ProducerCommand(Command):
def __init__(self,pid=None,pname=None):
if not (pid or pname) or (pid and pname):
raise AniDBIncorrectParameterError,"You must provide <p(id|name)> for PRODUCER command"
parameters={'pid':pid,'pname':pname}
Command.__init__(self,'PRODUCER',**parameters)
def cached(self,intr,db):
pid=self.parameters['pid']
pname=self.parameters['pname']
codes=('pid', 'name', 'shortname', 'othername', 'type', 'pic', 'url')
names=','.join([code for code in codes if code!=''])
ruleholder=(pid and 'pid=%s' or '(name=%s OR shortname=%s OR othername=%s)')
rulevalues=(pid and [pid] or [pname,pname,pname])
rows=db.select('ptb',names,ruleholder+" AND status&8",*rulevalues)
if len(rows)>1:
raise AniDBInternalError,"It shouldn't be possible for database to return more than 1 line for PRODUCER cache"
elif not len(rows):
return None
else:
resp=ProducerResponse(self,None,'245','CACHED PRODUCER',[list(rows[0])])
resp.parse()
return resp
def cache(self,intr,db):
if self.resp.rescode!='245' or self.cached(intr,db):
return
codes=('pid', 'name', 'shortname', 'othername', 'type', 'pic', 'url')
if len(db.select('ptb','pid','pid=%s',self.resp.datalines[0]['pid'])):
sets='status=status|15,'+','.join([code+'=%s' for code in codes if code!=''])
values=[self.resp.datalines[0][code] for code in codes if code!='']+[self.resp.datalines[0]['pid']]
db.update('ptb',sets,'pid=%s',*values)
else:
names='status,'+','.join([code for code in codes if code!=''])
valueholders='0,'+','.join(['%s'for code in codes if code!=''])
values=[self.resp.datalines[0][code] for code in codes if code!='']
db.insert('ptb',names,valueholders,*values)
class MyListCommand(Command):
def __init__(self,lid=None,fid=None,size=None,ed2k=None,aid=None,aname=None,gid=None,gname=None,epno=None):
if not (lid or fid or (size and ed2k) or (aid or aname)) or (lid and (fid or size or ed2k or aid or aname or gid or gname or epno)) or (fid and (lid or size or ed2k or aid or aname or gid or gname or epno)) or ((size and ed2k) and (lid or fid or aid or aname or gid or gname or epno)) or ((aid or aname) and (lid or fid or size or ed2k)) or (aid and aname) or (gid and gname):
raise AniDBIncorrectParameterError,"You must provide <lid XOR fid XOR size+ed2k XOR a(id|name)+g(id|name)+epno> for MYLIST command"
parameters={'lid':lid,'fid':fid,'size':size,'ed2k':ed2k,'aid':aid,'aname':aname,'gid':gid,'gname':gname,'epno':epno}
Command.__init__(self,'MYLIST',**parameters)
def cached(self,intr,db):
lid=self.parameters['lid']
fid=self.parameters['fid']
size=self.parameters['size']
ed2k=self.parameters['ed2k']
aid=self.parameters['aid']
aname=self.parameters['aname']
gid=self.parameters['gid']
gname=self.parameters['gname']
epno=self.parameters['epno']
names=','.join([code for code in MylistResponse(None,None,None,None,[]).codetail if code!=''])
if lid:
ruleholder="lid=%s"
rulevalues=[lid]
elif fid or size or ed2k:
resp=intr.file(fid=fid,size=size,ed2k=ed2k)
if resp.rescode!='220':
resp=NoSuchMylistResponse(self,None,'321','NO SUCH ENTRY (FILE NOT FOUND)',[])
resp.parse()
return resp
fid=resp.datalines[0]['fid']
ruleholder="fid=%s"
rulevalues=[fid]
else:
resp=intr.anime(aid=aid,aname=aname)
if resp.rescode!='230':
resp=NoSuchFileResponse(self,None,'321','NO SUCH ENTRY (ANIME NOT FOUND)',[])
resp.parse()
return resp
aid=resp.datalines[0]['aid']
resp=intr.group(gid=gid,gname=gname)
if resp.rescode!='250':
resp=NoSuchFileResponse(self,None,'321','NO SUCH ENTRY (GROUP NOT FOUND)',[])
resp.parse()
return resp
gid=resp.datalines[0]['gid']
resp=intr.episode(aid=aid,epno=epno)
if resp.rescode!='240':
resp=NoSuchFileResponse(self,None,'321','NO SUCH ENTRY (EPISODE NOT FOUND)',[])
resp.parse()
return resp
eid=resp.datalines[0]['eid']
ruleholder="aid=%s AND eid=%s AND gid=%s"
rulevalues=[aid,eid,gid]
rows=db.select('ltb',names,ruleholder+" AND status&8",*rulevalues)
if len(rows)>1:
#resp=MultipleFilesFoundResponse(self,None,'322','CACHED MULTIPLE FILES FOUND',/*get fids from rows, not gonna do this as you haven't got a real cache out of these..*/)
return None
elif not len(rows):
return None
else:
resp=MylistResponse(self,None,'221','CACHED MYLIST',[list(rows[0])])
resp.parse()
return resp
def cache(self,intr,db):
if self.resp.rescode!='221' or self.cached(intr,db):
return
codes=MylistResponse(None,None,None,None,[]).codetail
if len(db.select('ltb','lid','lid=%s',self.resp.datalines[0]['lid'])):
sets='status=status|15,'+','.join([code+'=%s' for code in codes if code!=''])
values=[self.resp.datalines[0][code] for code in codes if code!='']+[self.resp.datalines[0]['lid']]
db.update('ltb',sets,'lid=%s',*values)
else:
names='status,'+','.join([code for code in codes if code!=''])
valueholders='15,'+','.join(['%s' for code in codes if code!=''])
values=[self.resp.datalines[0][code] for code in codes if code!='']
db.insert('ltb',names,valueholders,*values)
class MyListAddCommand(Command):
def __init__(self,lid=None,fid=None,size=None,ed2k=None,aid=None,aname=None,gid=None,gname=None,epno=None,edit=None,state=None,viewed=None,source=None,storage=None,other=None):
if not (lid or fid or (size and ed2k) or ((aid or aname) and (gid or gname))) or (lid and (fid or size or ed2k or aid or aname or gid or gname or epno)) or (fid and (lid or size or ed2k or aid or aname or gid or gname or epno)) or ((size and ed2k) and (lid or fid or aid or aname or gid or gname or epno)) or (((aid or aname) and (gid or gname)) and (lid or fid or size or ed2k)) or (aid and aname) or (gid and gname) or (lid and not edit):
raise AniDBIncorrectParameterError,"You must provide <lid XOR fid XOR size+ed2k XOR a(id|name)+g(id|name)+epno> for MYLISTADD command"
parameters={'lid':lid,'fid':fid,'size':size,'ed2k':ed2k,'aid':aid,'aname':aname,'gid':gid,'gname':gname,'epno':epno,'edit':edit,'state':state,'viewed':viewed,'source':source,'storage':storage,'other':other}
Command.__init__(self,'MYLISTADD',**parameters)
class MyListDelCommand(Command):
def __init__(self,lid=None,fid=None,aid=None,aname=None,gid=None,gname=None,epno=None):
if not (lid or fid or ((aid or aname) and (gid or gname) and epno)) or (lid and (fid or aid or aname or gid or gname or epno)) or (fid and (lid or aid or aname or gid or gname or epno)) or (((aid or aname) and (gid or gname) and epno) and (lid or fid)) or (aid and aname) or (gid and gname):
raise AniDBIncorrectParameterError,"You must provide <lid+edit=1 XOR fid XOR a(id|name)+g(id|name)+epno> for MYLISTDEL command"
parameters={'lid':lid,'fid':fid,'aid':aid,'aname':aname,'gid':gid,'gname':gname,'epno':epno}
Command.__init__(self,'MYLISTDEL',**parameters)
class MyListStatsCommand(Command):
def __init__(self):
Command.__init__(self,'MYLISTSTATS')
class VoteCommand(Command):
def __init__(self,type,id=None,name=None,value=None,epno=None):
if not (id or name) or (id and name):
raise AniDBIncorrectParameterError,"You must provide <(id|name)> for VOTE command"
parameters={'type':type,'id':id,'name':name,'value':value,'epno':epno}
Command.__init__(self,'VOTE',**parameters)
class RandomAnimeCommand(Command):
def __init__(self,type):
parameters={'type':type}
Command.__init__(self,'RANDOMANIME',**parameters)
class PingCommand(Command):
def __init__(self):
Command.__init__(self,'PING')
#second run
class EncryptCommand(Command):
def __init__(self,user,apipassword,type):
self.apipassword=apipassword
parameters={'user':user.lower(),'type':type}
Command.__init__(self,'ENCRYPT',**parameters)
class EncodingCommand(Command):
def __init__(self,name):
parameters={'name':type}
Command.__init__(self,'ENCODING',**parameters)
class SendMsgCommand(Command):
def __init__(self,to,title,body):
if len(title)>50 or len(body)>900:
raise AniDBIncorrectParameterError,"Title must not be longer than 50 chars and body must not be longer than 900 chars for SENDMSG command"
parameters={'to':to.lower(),'title':title,'body':body}
Command.__init__(self,'SENDMSG',**parameters)
class UserCommand(Command):
def __init__(self,user):
parameters={'user':user}
Command.__init__(self,'USER',**parameters)
class UptimeCommand(Command):
def __init__(self):
Command.__init__(self,'UPTIME')
class VersionCommand(Command):
def __init__(self):
Command.__init__(self,'VERSION')

37
lib/adba/aniDBerrors.py Normal file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
class AniDBError(Exception):
pass
class AniDBIncorrectParameterError(AniDBError):
pass
class AniDBCommandTimeoutError(AniDBError):
pass
class AniDBMustAuthError(AniDBError):
pass
class AniDBPacketCorruptedError(AniDBError):
pass
class AniDBBannedError(AniDBError):
pass
class AniDBInternalError(AniDBError):
pass

75
lib/adba/aniDBfileInfo.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import hashlib
import os
import xml.etree.cElementTree as etree
# http://www.radicand.org/blog/orz/2010/2/21/edonkey2000-hash-in-python/
def get_file_hash(filePath):
""" Returns the ed2k hash of a given file."""
if not filePath:
return None
md4 = hashlib.new('md4').copy
def gen(f):
while True:
x = f.read(9728000)
if x: yield x
else: return
def md4_hash(data):
m = md4()
m.update(data)
return m
with open(filePath, 'rb') as f:
a = gen(f)
hashes = [md4_hash(data).digest() for data in a]
if len(hashes) == 1:
return hashes[0].encode("hex")
else: return md4_hash(reduce(lambda a,d: a + d, hashes, "")).hexdigest()
def get_file_size(path):
size = os.path.getsize(path)
return size
def read_anidb_xml(filePath):
if not filePath:
filePath = os.path.join(os.path.dirname(os.path.abspath( __file__ )), "animetitles.xml")
return read_xml_into_etree(filePath)
def read_tvdb_map_xml(filePath):
if not filePath:
filePath = os.path.join(os.path.dirname(os.path.abspath( __file__ )), "anime-list.xml")
return read_xml_into_etree(filePath)
def read_xml_into_etree(filePath):
if not filePath:
return None
f = open(filePath,"r")
xmlASetree = etree.ElementTree(file = f)
return xmlASetree

218
lib/adba/aniDBlink.py Normal file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
import socket, sys, zlib
from time import time, sleep
import threading
from aniDBresponses import ResponseResolver
from aniDBerrors import *
class AniDBLink(threading.Thread):
def __init__(self, server, port, myport, logFunction, delay=2, timeout=20, logPrivate=False):
super(AniDBLink, self).__init__()
self.server = server
self.port = port
self.target = (server, port)
self.timeout = timeout
self.myport = 0
self.bound = self.connectSocket(myport, self.timeout)
self.cmd_queue = {None:None}
self.resp_tagged_queue = {}
self.resp_untagged_queue = []
self.tags = []
self.lastpacket = time()
self.delay = delay
self.session = None
self.banned = False
self.crypt = None
self.log = logFunction
self.logPrivate = logPrivate
self._stop = threading.Event()
self._quiting = False
self.setDaemon(True)
self.start()
def connectSocket(self, myport, timeout):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.settimeout(timeout)
portlist = [myport] + [7654]
for port in portlist:
try:
self.sock.bind(('', port))
except:
continue
else:
self.myport = port
return True
else:
return False;
def disconnectSocket(self):
self.sock.close()
def stop (self):
self.log("Releasing socket and stopping link thread")
self._quiting = True
self.disconnectSocket()
self._stop.set()
def stopped (self):
return self._stop.isSet()
def print_log(self, data):
print data
def print_log_dummy(self, data):
pass
def run(self):
while not self._quiting:
try:
data = self.sock.recv(8192)
except socket.timeout:
self._handle_timeouts()
continue
self.log("NetIO < %s" % repr(data))
try:
for i in range(2):
try:
tmp = data
resp = None
if tmp[:2] == '\x00\x00':
tmp = zlib.decompressobj().decompress(tmp[2:])
self.log("UnZip | %s" % repr(tmp))
resp = ResponseResolver(tmp)
except:
sys.excepthook(*sys.exc_info())
self.crypt = None
self.session = None
else:
break
if not resp:
raise AniDBPacketCorruptedError, "Either decrypting, decompressing or parsing the packet failed"
cmd = self._cmd_dequeue(resp)
resp = resp.resolve(cmd)
resp.parse()
if resp.rescode in ('200', '201'):
self.session = resp.attrs['sesskey']
if resp.rescode in ('209',):
print "sorry encryption is not supported"
raise
#self.crypt=aes(md5(resp.req.apipassword+resp.attrs['salt']).digest())
if resp.rescode in ('203', '403', '500', '501', '503', '506'):
self.session = None
self.crypt = None
if resp.rescode in ('504', '555'):
self.banned = True
print "AniDB API informs that user or client is banned:", resp.resstr
resp.handle()
if not cmd or not cmd.mode:
self._resp_queue(resp)
else:
self.tags.remove(resp.restag)
except:
sys.excepthook(*sys.exc_info())
print "Avoiding flood by paranoidly panicing: Aborting link thread, killing connection, releasing waiters and quiting"
self.sock.close()
try:cmd.waiter.release()
except:pass
for tag, cmd in self.cmd_queue.iteritems():
try:cmd.waiter.release()
except:pass
sys.exit()
def _handle_timeouts(self):
willpop = []
for tag, cmd in self.cmd_queue.iteritems():
if not tag:
continue
if time() - cmd.started > self.timeout:
self.tags.remove(cmd.tag)
willpop.append(cmd.tag)
cmd.waiter.release()
for tag in willpop:
self.cmd_queue.pop(tag)
def _resp_queue(self, response):
if response.restag:
self.resp_tagged_queue[response.restag] = response
else:
self.resp_untagged_queue.append(response)
def getresponse(self, command):
if command:
resp = self.resp_tagged_queue.pop(command.tag)
else:
resp = self.resp_untagged_queue.pop()
self.tags.remove(resp.restag)
return resp
def _cmd_queue(self, command):
self.cmd_queue[command.tag] = command
self.tags.append(command.tag)
def _cmd_dequeue(self, resp):
if not resp.restag:
return None
else:
return self.cmd_queue.pop(resp.restag)
def _delay(self):
return (self.delay < 2.1 and 2.1 or self.delay)
def _do_delay(self):
age = time() - self.lastpacket
delay = self._delay()
if age <= delay:
sleep(delay - age)
def _send(self, command):
if self.banned:
self.log("NetIO | BANNED")
raise AniDBBannedError, "Not sending, banned"
self._do_delay()
self.lastpacket = time()
command.started = time()
data = command.raw_data()
self.sock.sendto(data, self.target)
if command.command == 'AUTH' and self.logPrivate:
self.log("NetIO > sensitive data is not logged!")
else:
self.log("NetIO > %s" % repr(data))
def new_tag(self):
if not len(self.tags):
maxtag = "T000"
else:
maxtag = max(self.tags)
newtag = "T%03d" % (int(maxtag[1:]) + 1)
return newtag
def request(self, command):
if not (self.session and command.session) and command.command not in ('AUTH', 'PING', 'ENCRYPT'):
raise AniDBMustAuthError, "You must be authed to execute commands besides AUTH and PING"
command.started = time()
self._cmd_queue(command)
self._send(command)

138
lib/adba/aniDBmaper.py Normal file
View File

@ -0,0 +1,138 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
from random import shuffle
class AniDBMaper:
blacklist = ('unused','retired','reserved')
def getAnimeBitsA(self,amask):
map = self.getAnimeMapA()
return self._getBitChain(map,amask);
def getAnimeCodesA(self,aBitChain):
amap = self.getAnimeMapA()
return self._getCodes(amap,aBitChain);
def getFileBitsF(self,fmask):
fmap = self.getFileMapF()
return self._getBitChain(fmap,fmask)
def getFileCodesF(self,bitChainF):
fmap = self.getFileMapF()
return self._getCodes(fmap,bitChainF)
def getFileBitsA(self,amask):
amap = self.getFileMapA()
return self._getBitChain(amap,amask)
def getFileCodesA(self,bitChainA):
amap = self.getFileMapA()
return self._getCodes(amap,bitChainA)
def _getBitChain(self,map,wanted):
"""Return an hex string with the correct bit set corresponding to the wanted fields in the map
"""
bit = 0
for index,field in enumerate(map):
if field in wanted and not field in self.blacklist:
bit = bit ^ (1<<len(map)-index-1)
bit = str(hex(bit)).lstrip("0x").rstrip("L")
bit = ''.join(["0" for unused in xrange(len(map)/4 - len(bit))])+bit
return bit
def _getCodes(self,map,bitChain):
"""Returns a list with the corresponding fields as set in the bitChain (hex string)
"""
codeList=[]
bitChain = int(bitChain,16)
mapLength = len(map)
for i in reversed(range(mapLength)):
if bitChain&(2**i):
codeList.append(map[mapLength-i-1])
return codeList
def getAnimeMapA(self):
# each line is one byte
# only chnage this if the api changes
map = ['aid','unused','year','type','related_aid_list','related_aid_type','category_list','category_weight_list',
'romaji_name','kanji_name','english_name','other_name','short_name_list','synonym_list','retired','retired',
'episodes','highest_episode_number','special_ep_count','air_date','end_date','url','picname','category_id_list',
'rating','vote_count','temp_rating','temp_vote_count','average_review_rating','review_count','award_list','is_18_restricted',
'anime_planet_id','ANN_id','allcinema_id','AnimeNfo_id','unused','unused','unused','date_record_updated',
'character_id_list','creator_id_list','main_creator_id_list','main_creator_name_list','unused','unused','unused','unused',
'specials_count','credits_count','other_count','trailer_count','parody_count','unused','unused','unused']
return map
def getFileMapF(self):
# each line is one byte
# only chnage this if the api changes
map = ['unused','aid','eid','gid','mylist_id','list_other_episodes','IsDeprecated','state',
'size','ed2k','md5','sha1','crc32','unused','unused','reserved',
'quality','source','audio_codec_list','audio_bitrate_list','video_codec','video_bitrate','video_resolution','file_type_extension',
'dub_language','sub_language','length_in_seconds','description','aired_date','unused','unused','anidb_file_name',
'mylist_state','mylist_filestate','mylist_viewed','mylist_viewdate','mylist_storage','mylist_source','mylist_other','unused']
return map
def getFileMapA(self):
# each line is one byte
# only chnage this if the api changes
map = ['anime_total_episodes','highest_episode_number','year','type','related_aid_list','related_aid_type','category_list','reserved',
'romaji_name','kanji_name','english_name','other_name','short_name_list','synonym_list','retired','retired',
'epno','ep_name','ep_romaji_name','ep_kanji_name','episode_rating','episode_vote_count','unused','unused',
'group_name','group_short_name','unused','unused','unused','unused','unused','date_aid_record_updated']
return map
def checkMapping(self,verbos=False):
print "------"
print "File F: "+ str(self.checkMapFileF(verbos))
print "------"
print "File A: "+ str(self.checkMapFileA(verbos))
def checkMapFileF(self,verbos=False):
getGeneralMap = lambda: self.getFileMapF()
getBits = lambda x: self.getFileBitsF(x)
getCodes = lambda x: self.getFileCodesF(x)
return self._checkMapGeneral(getGeneralMap,getBits,getCodes,verbos=verbos)
def checkMapFileA(self,verbos=False):
getGeneralMap = lambda: self.getFileMapA()
getBits = lambda x: self.getFileBitsA(x)
getCodes = lambda x: self.getFileCodesA(x)
return self._checkMapGeneral(getGeneralMap,getBits,getCodes,verbos=verbos)
def _checkMapGeneral(self,getGeneralMap,getBits,getCodes,verbos=False):
map = getGeneralMap()
shuffle(map)
mask = [elem for elem in map if elem not in self.blacklist][:5]
bits = getBits(mask)
mask_re = getCodes(bits)
bits_re = getBits(mask_re)
if verbos:
print mask
print mask_re
print bits
print bits_re
print "bits are:"+ str((bits_re == bits))
print "map is :"+ str((sorted(mask_re) == sorted(mask)))
return (bits_re == bits) and sorted(mask_re) == sorted(mask)

1856
lib/adba/aniDBresponses.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python
#
# This file is part of aDBa.
#
# aDBa 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.
#
# aDBa 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 aDBa. If not, see <http://www.gnu.org/licenses/>.
import os
import xml.etree.cElementTree as etree
import aniDBfileInfo as fileInfo
class TvDBMap():
def __init__(self,filePath=None):
self.xmlMap = fileInfo.read_tvdb_map_xml(filePath)
def get_tvdb_for_anidb(self,anidb_id):
return self._get_x_for_y(anidb_id,"anidbid","tvdbid")
def get_anidb_for_tvdb(self,tvdb_id):
return self._get_x_for_y(tvdb_id,"tvdbid","anidbid")
def _get_x_for_y(self,xValue,x,y):
#print("searching "+x+" with the value "+str(xValue)+" and want to give back "+y)
xValue = str(xValue)
for anime in self.xmlMap.findall("anime"):
try:
if anime.get(x,False) == xValue:
return int(anime.get(y,0))
except ValueError, e:
continue
return 0
def get_season_episode_for_anidb_absoluteNumber(self,anidb_id,absoluteNumber):
# NOTE: this cant be done without the length of each season from thetvdb
#TODO: implement
season = 0
episode = 0
for anime in self.xmlMap.findall("anime"):
if int(anime.get("anidbid",False)) == anidb_id:
defaultSeason = int(anime.get("defaulttvdbseason",1))
return (season,episode)
def get_season_episode_for_tvdb_absoluteNumber(self,anidb_id,absoluteNumber):
#TODO: implement
season = 0
episode = 0
return (season,episode)

5250
lib/adba/anime-list.xml Normal file

File diff suppressed because it is too large Load Diff

49439
lib/adba/animetitles.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -418,7 +418,7 @@ class TVRage:
'airtime': 'airs_time',
'airday': 'airs_dayofweek',
'image': 'fanart',
'epnum': 'id',
'epnum': 'absolute_number',
'title': 'episodename',
'airdate': 'firstaired',
'screencap': 'filename',

View File

@ -32,7 +32,7 @@ from threading import Lock
from sickbeard import providers, metadata, config
from sickbeard.providers.generic import GenericProvider
from providers import ezrss, tvtorrents, btn, newznab, womble, thepiratebay, torrentleech, kat, publichd, iptorrents, \
omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, nextgen, speedcd
omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, nextgen, speedcd, nyaatorrents, fanzub
from sickbeard.config import CheckSection, check_setting_int, check_setting_str, check_setting_float, ConfigMigrator, \
naming_ep_type
from sickbeard import searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser, \
@ -159,6 +159,7 @@ FLATTEN_FOLDERS_DEFAULT = None
SUBTITLES_DEFAULT = None
INDEXER_DEFAULT = None
INDEXER_TIMEOUT = None
ANIME_DEFAULT = None
PROVIDER_ORDER = []
NAMING_MULTI_EP = None
@ -169,6 +170,7 @@ NAMING_SPORTS_PATTERN = None
NAMING_CUSTOM_SPORTS = None
NAMING_FORCE_FOLDERS = False
NAMING_STRIP_YEAR = None
NAMING_ANIME = None
USE_NZBS = None
USE_TORRENTS = None
@ -320,6 +322,14 @@ NMJ_HOST = None
NMJ_DATABASE = None
NMJ_MOUNT = None
ANIMESUPPORT = False
USE_ANIDB = False
ANIDB_USERNAME = None
ANIDB_PASSWORD = None
ANIDB_USE_MYLIST = 0
ADBA_CONNECTION = None
ANIME_SPLIT_HOME = False
USE_SYNOINDEX = False
USE_NMJv2 = False
@ -460,7 +470,9 @@ def initialize(consoleLogging=True):
METADATA_WDTV, METADATA_TIVO, METADATA_MEDE8ER, IGNORE_WORDS, CALENDAR_UNPROTECTED, CREATE_MISSING_SHOW_DIRS, \
ADD_SHOWS_WO_DIR, USE_SUBTITLES, SUBTITLES_LANGUAGES, SUBTITLES_DIR, SUBTITLES_SERVICES_LIST, SUBTITLES_SERVICES_ENABLED, SUBTITLES_HISTORY, SUBTITLES_FINDER_FREQUENCY, subtitlesFinderScheduler, \
USE_FAILED_DOWNLOADS, DELETE_FAILED, ANON_REDIRECT, LOCALHOST_IP, TMDB_API_KEY, DEBUG, PROXY_SETTING, \
AUTOPOSTPROCESSER_FREQUENCY, DEFAULT_AUTOPOSTPROCESSER_FREQUENCY, MIN_AUTOPOSTPROCESSER_FREQUENCY
AUTOPOSTPROCESSER_FREQUENCY, DEFAULT_AUTOPOSTPROCESSER_FREQUENCY, MIN_AUTOPOSTPROCESSER_FREQUENCY, \
ANIME_DEFAULT, NAMING_ANIME, ANIMESUPPORT, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \
ANIME_SPLIT_HOME
if __INITIALIZED__:
return False
@ -572,6 +584,7 @@ def initialize(consoleLogging=True):
FLATTEN_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'flatten_folders_default', 0))
INDEXER_DEFAULT = check_setting_int(CFG, 'General', 'indexer_default', 0)
INDEXER_TIMEOUT = check_setting_int(CFG, 'General', 'indexer_timeout', 10)
ANIME_DEFAULT = bool(check_setting_int(CFG, 'General', 'anime_default', 0))
PROVIDER_ORDER = check_setting_str(CFG, 'General', 'provider_order', '').split()
@ -584,6 +597,7 @@ def initialize(consoleLogging=True):
NAMING_MULTI_EP = check_setting_int(CFG, 'General', 'naming_multi_ep', 1)
NAMING_FORCE_FOLDERS = naming.check_force_season_folders()
NAMING_STRIP_YEAR = bool(check_setting_int(CFG, 'General', 'naming_strip_year', 0))
NAMING_ANIME = check_setting_int(CFG, 'General', 'naming_anime', 3)
USE_NZBS = bool(check_setting_int(CFG, 'General', 'use_nzbs', 0))
USE_TORRENTS = bool(check_setting_int(CFG, 'General', 'use_torrents', 1))
@ -842,6 +856,13 @@ def initialize(consoleLogging=True):
USE_LISTVIEW = bool(check_setting_int(CFG, 'General', 'use_listview', 0))
ANIMESUPPORT = False
USE_ANIDB = check_setting_str(CFG, 'ANIDB', 'use_anidb', '')
ANIDB_USERNAME = check_setting_str(CFG, 'ANIDB', 'anidb_username', '')
ANIDB_PASSWORD = check_setting_str(CFG, 'ANIDB', 'anidb_password', '')
ANIDB_USE_MYLIST = check_setting_str(CFG, 'ANIDB', 'anidb_use_mylist', '')
ANIME_SPLIT_HOME = bool(check_setting_int(CFG, 'ANIME', 'anime_split_home', 0))
METADATA_XBMC = check_setting_str(CFG, 'General', 'metadata_xbmc', '0|0|0|0|0|0|0|0|0|0')
METADATA_XBMC_12PLUS = check_setting_str(CFG, 'General', 'metadata_xbmc_12plus', '0|0|0|0|0|0|0|0|0|0')
METADATA_MEDIABROWSER = check_setting_str(CFG, 'General', 'metadata_mediabrowser', '0|0|0|0|0|0|0|0|0|0')
@ -1211,6 +1232,15 @@ def halt():
except:
pass
if ADBA_CONNECTION:
ADBA_CONNECTION.logout()
#ADBA_CONNECTION.stop()
logger.log(u"Waiting for the ANIDB CONNECTION thread to exit")
try:
ADBA_CONNECTION.join(5)
except:
pass
__INITIALIZED__ = False
@ -1362,6 +1392,7 @@ def save_config():
new_config['General']['flatten_folders_default'] = int(FLATTEN_FOLDERS_DEFAULT)
new_config['General']['indexer_default'] = int(INDEXER_DEFAULT)
new_config['General']['indexer_timeout'] = int(INDEXER_TIMEOUT)
new_config['General']['anime_default'] = int(ANIME_DEFAULT)
new_config['General']['provider_order'] = ' '.join(PROVIDER_ORDER)
new_config['General']['version_notify'] = int(VERSION_NOTIFY)
new_config['General']['auto_update'] = int(AUTO_UPDATE)
@ -1372,6 +1403,7 @@ def save_config():
new_config['General']['naming_custom_sports'] = int(NAMING_CUSTOM_SPORTS)
new_config['General']['naming_sports_pattern'] = NAMING_SPORTS_PATTERN
new_config['General']['naming_multi_ep'] = int(NAMING_MULTI_EP)
new_config['General']['naming_anime'] = int(NAMING_ANIME)
new_config['General']['launch_browser'] = int(LAUNCH_BROWSER)
new_config['General']['update_shows_on_start'] = int(UPDATE_SHOWS_ON_START)
new_config['General']['sort_article'] = int(SORT_ARTICLE)
@ -1711,6 +1743,15 @@ def save_config():
new_config['FailedDownloads']['use_failed_downloads'] = int(USE_FAILED_DOWNLOADS)
new_config['FailedDownloads']['delete_failed'] = int(DELETE_FAILED)
new_config['ANIDB'] = {}
new_config['ANIDB']['use_anidb'] = USE_ANIDB
new_config['ANIDB']['anidb_username'] = ANIDB_USERNAME
new_config['ANIDB']['anidb_password'] = helpers.encrypt(ANIDB_PASSWORD, ENCRYPTION_VERSION)
new_config['ANIDB']['anidb_use_mylist'] = ANIDB_USE_MYLIST
new_config['ANIME'] = {}
new_config['ANIME']['anime_split_home'] = int(ANIME_SPLIT_HOME)
new_config.write()

View File

@ -150,7 +150,7 @@ class Quality:
return (sorted(anyQualities), sorted(bestQualities))
@staticmethod
def nameQuality(name):
def nameQuality(name, anime=False):
"""
Return The quality from an episode File renamed by Sickbeard
If no quality is achieved it will try sceneQuality regex
@ -162,8 +162,9 @@ class Quality:
for x in sorted(Quality.qualityStrings.keys(), reverse=True):
if x == Quality.UNKNOWN:
continue
if x == Quality.NONE: #Last chance
return Quality.sceneQuality(name)
return Quality.sceneQuality(name, anime)
regex = '\W' + Quality.qualityStrings[x].replace(' ', '\W') + '\W'
regex_match = re.search(regex, name, re.I)
@ -171,7 +172,7 @@ class Quality:
return x
@staticmethod
def sceneQuality(name):
def sceneQuality(name, anime=False):
"""
Return The quality from the scene episode File
"""
@ -180,6 +181,26 @@ class Quality:
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
if anime:
blueRayOptions = checkName(["bluray", "blu-ray"], any)
hdOptions = checkName(["720p", "1280x720", "960x720"], any)
fullHD = checkName(["1080p", "1920x1080"], any)
if checkName(["360p", "XviD"], any):
return Quality.SDTV
elif checkName(["dvd", "480p", "848x480"], any):
return Quality.SDDVD
elif hdOptions and not blueRayOptions and not fullHD:
return Quality.HDTV
elif hdOptions and not blueRayOptions and not fullHD:
return Quality.HDWEBDL
elif blueRayOptions and hdOptions and not fullHD:
return Quality.HDBLURAY
elif fullHD:
return Quality.FULLHDBLURAY
else:
return Quality.UNKNOWN
if checkName(["(pdtv|hdtv|dsr|tvrip).(xvid|x264|h.?264)"], all) and not checkName(["(720|1080)[pi]"], all):
return Quality.SDTV
elif checkName(["web.dl|webrip", "xvid|x264|h.?264"], all) and not checkName(["(720|1080)[pi]"], all):

View File

@ -586,7 +586,7 @@ class ConfigMigrator():
logger.ERROR)
continue
if name == 'SickRage Index':
if name == 'Sick Beard Index':
key = '0'
if name == 'NZBs.org':

View File

@ -44,7 +44,7 @@ class AddSceneExceptions(InitialSchema):
def execute(self):
self.connection.action(
"CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, tvdb_id INTEGER KEY, show_name TEXT)")
"CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, indexer_id INTEGER KEY, show_name TEXT)")
class AddSceneNameCache(AddSceneExceptions):
@ -52,7 +52,7 @@ class AddSceneNameCache(AddSceneExceptions):
return self.hasTable("scene_names")
def execute(self):
self.connection.action("CREATE TABLE scene_names (tvdb_id INTEGER, name TEXT)")
self.connection.action("CREATE TABLE scene_names (indexer_id INTEGER, name TEXT)")
class AddNetworkTimezones(AddSceneNameCache):
@ -69,7 +69,7 @@ class AddXemNumbering(AddNetworkTimezones):
def execute(self):
self.connection.action(
"CREATE TABLE xem_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER)")
"CREATE TABLE xem_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, absolute_number INTEGER, scene_season INTEGER, scene_episode INTEGER, scene_absolute_number INTEGER, PRIMARY KEY (indexer_id, scene_season, scene_episode))")
class AddXemRefresh(AddXemNumbering):
def test(self):
@ -79,52 +79,24 @@ class AddXemRefresh(AddXemNumbering):
self.connection.action(
"CREATE TABLE xem_refresh (indexer TEXT, indexer_id INTEGER PRIMARY KEY, last_refreshed INTEGER)")
class ConvertSceneExceptionsToIndexerID(AddXemRefresh):
def test(self):
return self.hasColumn("scene_exceptions", "indexer_id")
def execute(self):
if self.hasTable("tmp_scene_exceptions"):
self.connection.action("DROP TABLE tmp_scene_exceptions")
self.connection.action("ALTER TABLE scene_exceptions RENAME TO tmp_scene_exceptions")
self.connection.action(
"CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, indexer_id INTEGER KEY, show_name TEXT)")
self.connection.action(
"INSERT INTO scene_exceptions(exception_id, indexer_id, show_name) SELECT exception_id, tvdb_id, show_name FROM tmp_scene_exceptions")
self.connection.action("DROP TABLE tmp_scene_exceptions")
class ConvertSceneNamesToIndexerID(ConvertSceneExceptionsToIndexerID):
def test(self):
return self.hasColumn("scene_names", "indexer_id")
def execute(self):
if self.hasTable("tmp_scene_names"):
self.connection.action("DROP TABLE tmp_scene_names")
self.connection.action("ALTER TABLE scene_names RENAME TO tmp_scene_names")
self.connection.action("CREATE TABLE scene_names (indexer_id INTEGER, name TEXT)")
self.connection.action("INSERT INTO scene_names(indexer_id, name) SELECT tvdb_id, name FROM tmp_scene_names")
self.connection.action("DROP TABLE tmp_scene_names")
class ConvertIndexerToInteger(ConvertSceneNamesToIndexerID):
def execute(self):
ql = []
ql.append(["UPDATE xem_numbering SET indexer = ? WHERE LOWER(indexer) = ?", ["1", "tvdb"]])
ql.append(["UPDATE xem_numbering SET indexer = ? WHERE LOWER(indexer) = ?", ["2", "tvrage"]])
ql.append(["UPDATE xem_refresh SET indexer = ? WHERE LOWER(indexer) = ?", ["1", "tvdb"]])
ql.append(["UPDATE xem_refresh SET indexer = ? WHERE LOWER(indexer) = ?", ["2", "tvrage"]])
self.connection.mass_action(ql)
class RemoveKeysFromXemNumbering(ConvertIndexerToInteger):
def execute(self):
self.connection.action("ALTER TABLE xem_numbering DROP UNIQUE (indexer, indexer_id, season, episode)")
self.connection.action("ALTER TABLE xem_numbering DROP PRIMARY KEY")
class AddLastSearch(RemoveKeysFromXemNumbering):
class AddLastSearch(AddXemRefresh):
def test(self):
return self.hasTable("lastSearch")
def execute(self):
self.connection.action("CREATE TABLE lastSearch (provider TEXT, time NUMERIC)")
class AddAbsoluteNumbering(AddLastSearch):
def test(self):
return self.hasColumn("xem_numbering", "absolute_number")
def execute(self):
self.addColumn("xem_numbering", "absolute_number", "NUMERIC", "0")
self.addColumn("xem_numbering", "scene_absolute_number", "NUMERIC", "0")
class AddSceneExceptionsSeasons(AddSceneNameCache):
def test(self):
return self.hasColumn("scene_exceptions", "season")
def execute(self):
self.addColumn("scene_exceptions", "season", "NUMERIC", -1)

View File

@ -27,8 +27,7 @@ from sickbeard import encodingKludge as ek
from sickbeard.name_parser.parser import NameParser, InvalidNameException
MIN_DB_VERSION = 9 # oldest db version we support migrating from
MAX_DB_VERSION = 31
MAX_DB_VERSION = 34
class MainSanityCheck(db.DBSanityCheck):
def check(self):
@ -714,7 +713,7 @@ class AddSceneNumbering(AddArchiveFirstMatchOption):
self.connection.action("DROP TABLE scene_numbering")
self.connection.action(
"CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, PRIMARY KEY (indexer_id, season, episode))")
"CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, PRIMARY KEY (indexer_id, season, episode, scene_season, scene_episode))")
self.incDBVersion()
@ -794,10 +793,44 @@ class AddSceneNumberingToTvEpisodes(AddSportsOption):
backupDatabase(31)
logger.log(u"Adding column scene_season and scene_episode to tvepisodes")
if not self.hasColumn("tv_episodes", "scene_season"):
self.addColumn("tv_episodes", "scene_season", "NUMERIC", -1)
self.addColumn("tv_episodes", "scene_season", "NUMERIC", "NULL")
self.addColumn("tv_episodes", "scene_episode", "NUMERIC", "NULL")
if not self.hasColumn("tv_episodes", "scene_episode"):
self.addColumn("tv_episodes", "scene_episode", "NUMERIC", -1)
self.incDBVersion()
self.incDBVersion()
class AddAnimeTVShow(AddSceneNumberingToTvEpisodes):
def test(self):
return self.checkDBVersion() >= 32
def execute(self):
backupDatabase(32)
logger.log(u"Adding column anime to tv_episodes")
self.addColumn("tv_shows", "anime", "NUMERIC", "0")
self.incDBVersion()
class AddAbsoluteNumbering(AddAnimeTVShow):
def test(self):
return self.checkDBVersion() >= 33
def execute(self):
backupDatabase(33)
logger.log(u"Adding column absolute_number to tv_episodes")
self.addColumn("tv_episodes", "absolute_number", "NUMERIC", "0")
self.incDBVersion()
class AddSceneAbsoluteNumbering(AddAbsoluteNumbering):
def test(self):
return self.checkDBVersion() >= 34
def execute(self):
backupDatabase(34)
logger.log(u"Adding column absolute_number and scene_absolute_number to scene_numbering")
self.addColumn("scene_numbering", "absolute_number", "NUMERIC", "0")
self.addColumn("scene_numbering", "scene_absolute_number", "NUMERIC", "0")
self.incDBVersion()

View File

@ -120,8 +120,13 @@ class DBConnection:
sqlResult = []
attempt = 0
# Transaction
self.connection.isolation_level = None
self.connection.execute('begin')
while attempt < 5:
try:
for qu in querylist:
if len(qu) == 1:
if logTransaction:
@ -131,7 +136,9 @@ class DBConnection:
if logTransaction:
logger.log(qu[0] + " with args " + str(qu[1]), logger.DEBUG)
sqlResult.append(self.connection.execute(qu[0], qu[1]))
self.connection.commit()
self.connection.execute('commit')
logger.log(u"Transaction with " + str(len(querylist)) + u" queries executed", logger.DEBUG)
return sqlResult
except sqlite3.OperationalError, e:

View File

@ -18,6 +18,7 @@
from sickbeard.encodingKludge import fixStupidEncodings
def ex(e):
"""
Returns a unicode string from the exception text if it exists.
@ -136,4 +137,8 @@ class FailedHistoryMultiSnatchException(SickBeardException):
class FailedHistoryNotFoundException(SickBeardException):
"The release was not found in the failed download history tracker"
"The release was not found in the failed download history tracker"
class EpisodeNotFoundByAbsoluteNumberException(SickBeardException):
"The show wasn't found in the DB while looking at Absolute Numbers"

View File

@ -50,13 +50,14 @@ except ImportError:
from xml.dom.minidom import Node
import sickbeard
from sickbeard.exceptions import MultipleShowObjectsException, ex
from sickbeard.exceptions import MultipleShowObjectsException, EpisodeNotFoundByAbsoluteNumberException, ex
from sickbeard import logger, classes
from sickbeard.common import USER_AGENT, mediaExtensions, subtitleExtensions, XML_NSMAP
from sickbeard import db
from sickbeard import encodingKludge as ek
from sickbeard import notifiers
from lib import subliminal
from lib import adba
urllib._urlopener = classes.SickBeardURLopener()
@ -186,8 +187,7 @@ def getURL(url, post_data=None, headers=None, params=None, timeout=30, json=Fals
url = urlparse.urlunparse(parsed)
it = iter(req_headers)
if use_proxy and sickbeard.PROXY_SETTING:
logger.log("Using proxy for url: " + url, logger.DEBUG)
proxies = {
@ -317,13 +317,15 @@ def searchDBForShow(regShowName, log=False):
continue
elif len(sqlResults) > 1:
if log:
logger.log(u"Multiple results for " + showName + " in the DB, unable to match show name", logger.DEBUG)
logger.log(u"Multiple results for " + showName + " in the DB, unable to match show name",
logger.DEBUG)
continue
else:
return (int(sqlResults[0]["indexer_id"]), sqlResults[0]["show_name"])
return
def searchIndexerForShowID(regShowName, indexer=None, indexer_id=None, ui=None):
showNames = list(set([re.sub('[. -]', ' ', regShowName), regShowName]))
@ -337,11 +339,16 @@ def searchIndexerForShowID(regShowName, indexer=None, indexer_id=None, ui=None):
for name in showNames:
logger.log(u"Trying to find " + name + " on " + sickbeard.indexerApi(i).name, logger.DEBUG)
search = t[indexer_id] if indexer_id else t[name]
try:
search = t[indexer_id] if indexer_id else t[name]
except:
continue
try:
seriesname = search.seriesname
except:
seriesname = None
try:
series_id = search.id
except:
@ -360,6 +367,7 @@ def searchIndexerForShowID(regShowName, indexer=None, indexer_id=None, ui=None):
return (None, None, None)
def sizeof_fmt(num):
'''
>>> sizeof_fmt(2)
@ -508,7 +516,7 @@ def rename_ep_file(cur_path, new_path, old_path_length=0):
old_path_length: The length of media file path (old name) WITHOUT THE EXTENSION
"""
new_dest_dir, new_dest_name = os.path.split(new_path) #@UnusedVariable
new_dest_dir, new_dest_name = os.path.split(new_path) # @UnusedVariable
if old_path_length == 0 or old_path_length > len(cur_path):
# approach from the right
@ -519,7 +527,7 @@ def rename_ep_file(cur_path, new_path, old_path_length=0):
cur_file_name = cur_path[:old_path_length]
if cur_file_ext[1:] in subtitleExtensions:
#Extract subtitle language from filename
# Extract subtitle language from filename
sublang = os.path.splitext(cur_file_name)[1][1:]
#Check if the language extracted from filename is a valid language
@ -662,6 +670,41 @@ def fixSetGroupID(childPath):
childPath, parentGID), logger.ERROR)
def is_anime_in_show_list():
for show in sickbeard.showList:
if show.is_anime:
return True
return False
def update_anime_support():
sickbeard.ANIMESUPPORT = is_anime_in_show_list()
def get_all_episodes_from_absolute_number(show, indexer_id, absolute_numbers):
if len(absolute_numbers) == 0:
raise EpisodeNotFoundByAbsoluteNumberException()
episodes = []
season = None
if not show and not indexer_id:
return (season, episodes)
if not show and indexer_id:
show = findCertainShow(sickbeard.showList, indexer_id)
for absolute_number in absolute_numbers:
ep = show.getEpisode(None, None, absolute_number=absolute_number)
if ep:
episodes.append(ep.episode)
else:
raise EpisodeNotFoundByAbsoluteNumberException()
season = ep.season # this will always take the last found seson so eps that cross the season border are not handeled well
return (season, episodes)
def sanitizeSceneName(name, ezrss=False):
"""
Takes a show name and returns the "scenified" version of it.
@ -947,11 +990,13 @@ def full_sanitizeSceneName(name):
return re.sub('[. -]', ' ', sanitizeSceneName(name)).lower().lstrip()
def _check_against_names(name, show):
nameInQuestion = full_sanitizeSceneName(name)
def _check_against_names(nameInQuestion, show, season=-1):
showNames = [show.name]
showNames.extend(sickbeard.scene_exceptions.get_scene_exceptions(show.indexerid))
showNames = []
if season in [-1, 1]:
showNames = [show.name]
showNames.extend(sickbeard.scene_exceptions.get_scene_exceptions(show.indexerid, season=season))
for showName in showNames:
nameFromList = full_sanitizeSceneName(showName)
@ -961,19 +1006,28 @@ def _check_against_names(name, show):
return False
def get_show_by_name(name):
def get_show_by_name(name, useIndexer=False):
name = full_sanitizeSceneName(name)
showObj = sickbeard.name_cache.retrieveShowFromCache(name)
if not showObj:
if not showObj and sickbeard.showList:
showNames = list(set(sickbeard.show_name_helpers.sceneToNormalShowNames(name)))
for showName in showNames if sickbeard.showList else []:
sceneResults = [x for x in sickbeard.showList if _check_against_names(showName, x)]
showObj = sceneResults[0] if len(sceneResults) else None
if showObj:
break
for showName in showNames:
if showName in sickbeard.scene_exceptions.exceptionIndexerCache:
showObj = findCertainShow(sickbeard.showList, int(sickbeard.scene_exceptions.exceptionIndexerCache[showName]))
if showObj:
break
if useIndexer and not showObj:
(sn, idx, id) = searchIndexerForShowID(showName, ui=classes.ShowListUI)
if id:
showObj = findCertainShow(sickbeard.showList, int(id))
if showObj:
break
return showObj
def is_hidden_folder(folder):
"""
Returns True if folder is hidden.
@ -986,6 +1040,7 @@ def is_hidden_folder(folder):
return False
def real_path(path):
"""
Returns: the canonicalized absolute pathname. The resulting path will have no symbolic link, '/./' or '/../' components.
@ -1009,3 +1064,28 @@ def validateShow(show, season=None, episode=None):
return t[show.indexerid][season][episode]
except (sickbeard.indexer_episodenotfound, sickbeard.indexer_seasonnotfound):
pass
def set_up_anidb_connection():
if not sickbeard.USE_ANIDB:
logger.log(u"Usage of anidb disabled. Skiping", logger.DEBUG)
return False
if not sickbeard.ANIDB_USERNAME and not sickbeard.ANIDB_PASSWORD:
logger.log(u"anidb username and/or password are not set. Aborting anidb lookup.", logger.DEBUG)
return False
if not sickbeard.ADBA_CONNECTION:
anidb_logger = lambda x: logger.log("ANIDB: " + str(x), logger.DEBUG)
sickbeard.ADBA_CONNECTION = adba.Connection(keepAlive=True, log=anidb_logger)
if not sickbeard.ADBA_CONNECTION.authed():
try:
sickbeard.ADBA_CONNECTION.auth(sickbeard.ANIDB_USERNAME, sickbeard.ANIDB_PASSWORD)
except Exception, e:
logger.log(u"exception msg: " + str(e))
return False
else:
return True
return sickbeard.ADBA_CONNECTION.authed()

View File

@ -11,7 +11,7 @@
# SickRage 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.
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickRage. If not, see <http://www.gnu.org/licenses/>.
@ -25,7 +25,7 @@ import time
import sickbeard
from sickbeard import logger, helpers, scene_numbering
from sickbeard.common import cpu_presets
from sickbeard.exceptions import EpisodeNotFoundByAbsoluteNumberException
from dateutil import parser
nameparser_lock = threading.Lock()
@ -35,13 +35,24 @@ class NameParser(object):
ALL_REGEX = 0
NORMAL_REGEX = 1
SPORTS_REGEX = 2
ANIME_REGEX = 3
def __init__(self, file_name=True, regexMode=0):
def __init__(self, file_name=True, show=None, useIndexers=False):
regexMode = self.ALL_REGEX
if show and show.is_anime:
regexMode = self.ANIME_REGEX
elif show and show.is_sports:
regexMode = self.SPORTS_REGEX
elif show and not show.is_anime and not show.is_sports:
regexMode = self.NORMAL_REGEX
self.file_name = file_name
self.regexMode = regexMode
self.compiled_regexes = []
self.compiled_regexes = {}
self._compile_regexes(self.regexMode)
self.showList = sickbeard.showList
self.useIndexers = useIndexers
def clean_series_name(self, series_name):
"""Cleans up series name by removing any . and _
@ -70,104 +81,128 @@ class NameParser(object):
def _compile_regexes(self, regexMode):
if regexMode <= self.ALL_REGEX:
logger.log(u"Using ALL regexs", logger.DEBUG)
uncompiled_regex = regexes.sports_regexs + regexes.ep_regexes
uncompiled_regex = [regexes.anime_regexes, regexes.sports_regexs, regexes.normal_regexes]
elif regexMode == self.NORMAL_REGEX:
logger.log(u"Using NORMAL regexs", logger.DEBUG)
uncompiled_regex = regexes.ep_regexes
uncompiled_regex = [regexes.normal_regexes]
elif regexMode == self.SPORTS_REGEX:
logger.log(u"Using SPORTS regexs", logger.DEBUG)
uncompiled_regex = regexes.sports_regexs
uncompiled_regex = [regexes.sports_regexs]
elif regexMode == self.ANIME_REGEX:
logger.log(u"Using ANIME regexs", logger.DEBUG)
uncompiled_regex = [regexes.anime_regexes, regexes.normal_regexes]
else:
logger.log(u"This is a programing ERROR. Fallback Using NORMAL regexs", logger.ERROR)
uncompiled_regex = regexes.ep_regexes
uncompiled_regex = [regexes.normal_regexes]
for (cur_pattern_name, cur_pattern) in uncompiled_regex:
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 regexItem in uncompiled_regex:
for regex_type, regex in regexItem.items():
try:
self.compiled_regexes[regex_type]
except:
self.compiled_regexes[regex_type] = {}
for (cur_pattern_name, cur_pattern) in regex:
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[regex_type].update({cur_pattern_name: cur_regex})
def _parse_string(self, name):
for cur_regex_type, cur_regexes in self.compiled_regexes.items() if name else []:
for cur_regex_name, cur_regex in cur_regexes.items():
match = cur_regex.match(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):
if not match:
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]
result = ParseResult(name)
result.which_regex = [cur_regex_name]
if 'sports_event_id' in named_groups:
sports_event_id = match.group('sports_event_id')
if sports_event_id:
result.sports_event_id = int(match.group('sports_event_id'))
named_groups = match.groupdict().keys()
if 'sports_event_name' in named_groups:
result.sports_event_name = match.group('sports_event_name')
if result.sports_event_name:
result.sports_event_name = self.clean_series_name(result.sports_event_name)
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 'ep_ab_num' in named_groups:
ep_ab_num = self._convert_number(match.group('ep_ab_num'))
if 'extra_ab_ep_num' in named_groups and match.group('extra_ab_ep_num'):
result.ab_episode_numbers = range(ep_ab_num,
self._convert_number(match.group('extra_ab_ep_num')) + 1)
else:
result.ab_episode_numbers = [ep_ab_num]
if 'sports_event_id' in named_groups:
sports_event_id = match.group('sports_event_id')
if sports_event_id:
result.sports_event_id = int(match.group('sports_event_id'))
if 'sports_event_name' in named_groups:
result.sports_event_name = match.group('sports_event_name')
if result.sports_event_name:
result.sports_event_name = self.clean_series_name(result.sports_event_name)
if 'sports_event_date' in named_groups:
sports_event_date = match.group('sports_event_date')
if sports_event_date:
try:
result.sports_event_date = parser.parse(sports_event_date, fuzzy=True).date()
except:
continue
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'))
if 'sports_event_date' in named_groups:
sports_event_date = match.group('sports_event_date')
if sports_event_date:
try:
result.sports_event_date = parser.parse(sports_event_date, fuzzy=True).date()
dtStr = '%s-%s-%s' % (year, month, day)
result.air_date = datetime.datetime.strptime(dtStr, "%Y-%m-%d").date()
except:
continue
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'))
if 'extra_info' in named_groups:
tmp_extra_info = match.group('extra_info')
try:
dtStr = '%s-%s-%s' % (year, month, day)
result.air_date = datetime.datetime.strptime(dtStr, "%Y-%m-%d").date()
except:
continue
# Show.S04.Special or Show.S05.Part.2.Extras is almost certainly not every episode in the season
if tmp_extra_info and cur_regex_name == 'season_only' and re.search(
r'([. _-]|^)(special|extra)s?\w*([. _-]|$)', tmp_extra_info, re.I):
continue
result.extra_info = tmp_extra_info
if 'extra_info' in named_groups:
tmp_extra_info = match.group('extra_info')
if 'release_group' in named_groups:
result.release_group = match.group('release_group')
# Show.S04.Special or Show.S05.Part.2.Extras is almost certainly not every episode in the season
if tmp_extra_info and cur_regex_name == 'season_only' and re.search(r'([. _-]|^)(special|extra)s?\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
show = helpers.get_show_by_name(result.series_name, useIndexer=self.useIndexers)
if show and show.is_anime and cur_regex_type in ['anime', 'normal']:
result.show = show
return result
elif show and show.is_sports and cur_regex_type == 'sports':
result.show = show
return result
elif cur_regex_type == 'normal':
return result
return None
@ -248,6 +283,7 @@ class NameParser(object):
# build the ParseResult object
final_result.air_date = self._combine_results(file_name_result, dir_name_result, 'air_date')
final_result.ab_episode_numbers = self._combine_results(file_name_result, dir_name_result, 'ab_episode_numbers')
# sports event title
final_result.sports_event_id = self._combine_results(file_name_result, dir_name_result, 'sports_event_id')
@ -275,6 +311,20 @@ class NameParser(object):
if dir_name_result:
final_result.which_regex += dir_name_result.which_regex
final_result.show = self._combine_results(file_name_result, dir_name_result, 'show')
if final_result.show and final_result.show.is_anime and final_result.is_anime: # only need to to do another conversion if the scene2tvdb didn work
logger.log("Getting season and episodes from absolute numbers", logger.DEBUG)
try:
_actual_season, _actual_episodes = helpers.get_all_episodes_from_absolute_number(final_result.show,
None,
final_result.ab_episode_numbers)
except EpisodeNotFoundByAbsoluteNumberException:
logger.log(str(final_result.show.indexerid) + ": Indexer object absolute number " + str(
final_result.ab_episode_numbers) + " is incomplete, cant determin season and episode numbers")
else:
final_result.season = _actual_season
final_result.episodes = _actual_episodes
# 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, 'xmlcharrefreplace'))
@ -295,6 +345,8 @@ class ParseResult(object):
extra_info=None,
release_group=None,
air_date=None,
ab_episode_numbers=None,
show=None
):
self.original_name = original_name
@ -306,6 +358,11 @@ class ParseResult(object):
else:
self.episode_numbers = episode_numbers
if not ab_episode_numbers:
self.ab_episode_numbers = []
else:
self.ab_episode_numbers = ab_episode_numbers
self.extra_info = extra_info
self.release_group = release_group
@ -316,6 +373,7 @@ class ParseResult(object):
self.sports_event_date = sports_event_date
self.which_regex = None
self.show = show
def __eq__(self, other):
if not other:
@ -339,6 +397,10 @@ class ParseResult(object):
return False
if self.sports_event_date != other.sports_event_date:
return False
if self.ab_episode_numbers != other.ab_episode_numbers:
return False
if self.show != other.show:
return False
return True
@ -359,6 +421,8 @@ class ParseResult(object):
to_return += str(self.sports_event_name)
to_return += str(self.sports_event_id)
to_return += str(self.sports_event_date)
if self.ab_episode_numbers:
to_return += ' absolute_numbers: ' + str(self.ab_episode_numbers)
if self.extra_info:
to_return += ' - ' + self.extra_info
@ -367,23 +431,31 @@ class ParseResult(object):
to_return += ' [ABD: ' + str(self.air_by_date) + ']'
to_return += ' [SPORTS: ' + str(self.sports) + ']'
to_return += ' [ANIME: ' + str(self.is_anime) + ']'
to_return += ' [whichReg: ' + str(self.which_regex) + ']'
return to_return.encode('utf-8')
def convert(self, show):
if not show: return self # need show object
if not show: return self # need show object
if not self.season_number: return self # can't work without a season
if not len(self.episode_numbers): return self # need at least one episode
if self.air_by_date or self.sports: return self # scene numbering does not apply to air-by-date
new_episode_numbers = []
new_season_numbers = []
for epNo in self.episode_numbers:
(s, e) = scene_numbering.get_indexer_numbering(show.indexerid, show.indexer, self.season_number,
epNo)
new_absolute_numbers = []
for i, epNo in enumerate(self.episode_numbers):
abNo = None
if len(self.ab_episode_numbers):
abNo = self.ab_episode_numbers[i]
(s, e, a) = scene_numbering.get_indexer_numbering(show.indexerid, show.indexer, self.season_number,
epNo, abNo)
new_episode_numbers.append(e)
new_season_numbers.append(s)
new_absolute_numbers.append(a)
# need to do a quick sanity check here. It's possible that we now have episodes
# from more than one season (by tvdb numbering), and this is just too much
@ -400,6 +472,11 @@ class ParseResult(object):
new_episode_numbers = list(set(new_episode_numbers))
new_episode_numbers.sort()
# dedupe absolute numbers
new_absolute_numbers = list(set(new_absolute_numbers))
new_absolute_numbers.sort()
self.ab_episode_numbers = new_absolute_numbers
self.episode_numbers = new_episode_numbers
self.season_number = new_season_numbers[0]
@ -412,6 +489,13 @@ class ParseResult(object):
air_by_date = property(_is_air_by_date)
def _is_anime(self):
if self.ab_episode_numbers:
return True
return False
is_anime = property(_is_anime)
def _is_sports(self):
if self.sports_event_date:
return True

View File

@ -18,7 +18,7 @@
# all regexes are case insensitive
ep_regexes = [
normal_regexes = {'normal':[
('standard_repeat',
# Show.Name.S01E02.S01E03.Source.Quality.Etc-Group
# Show Name - S01E02 - S01E03 - S01E04 - Ep Name
@ -184,9 +184,9 @@ ep_regexes = [
-(?P<release_group>[^- ]+))?)?$ # Group
'''
),
]
]}
sports_regexs = [
sports_regexs = {'sports':[
('sports_standard',
# Sports.Name.2010.11.23.Source.Quality.Etc-Group
@ -200,4 +200,175 @@ sports_regexs = [
-(?P<release_group>[^- ]+))?)?$
'''
),
]
]}
anime_regexes = {'anime':[
('anime_ultimate',
"""
^(?:\[(?P<release_group>.+?)\][ ._-]*)
(?P<series_name>.+?)[ ._-]+
(?P<ep_ab_num>\d{1,3})
(-(?P<extra_ab_ep_num>\d{1,3}))?[ ._-]+?
(?:v(?P<version>[0-9]))?
(?:[\w\.]*)
(?:(?:(?:[\[\(])(?P<extra_info>\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)(?:[\]\)]))|(?:\d{3,4}[xp]))
(?:[ ._]?\[(?P<crc>\w+)\])?
.*?
"""
),
('anime_standard',
# [Group Name] Show Name.13-14
# [Group Name] Show Name - 13-14
# Show Name 13-14
# [Group Name] Show Name.13
# [Group Name] Show Name - 13
# Show Name 13
'''
^(\[(?P<release_group>.+?)\][ ._-]*)? # Release Group and separator
(?P<series_name>.+?)[ ._-]+ # Show_Name and separator
(?P<ep_ab_num>\d{1,3}) # E01
(-(?P<extra_ab_ep_num>\d{1,3}))? # E02
(v(?P<version>[0-9]))? # version
[ ._-]+\[(?P<extra_info>\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)\] # Source_Quality_Etc-
(\[(?P<crc>\w{8})\])? # CRC
.*? # Separator and EOL
'''),
('anime_standard_round',
# TODO examples
# [Stratos-Subs]_Infinite_Stratos_-_12_(1280x720_H.264_AAC)_[379759DB]
# [ShinBunBu-Subs] Bleach - 02-03 (CX 1280x720 x264 AAC)
'''
^(\[(?P<release_group>.+?)\][ ._-]*)? # Release Group and separator
(?P<series_name>.+?)[ ._-]+ # Show_Name and separator
(?P<ep_ab_num>\d{1,3}) # E01
(-(?P<extra_ab_ep_num>\d{1,3}))? # E02
(v(?P<version>[0-9]))? # version
[ ._-]+\((?P<extra_info>(CX[ ._-]?)?\d{3,4}[xp]?\d{0,4}[\.\w\s-]*)\) # Source_Quality_Etc-
(\[(?P<crc>\w{8})\])? # CRC
.*? # Separator and EOL
'''),
('anime_slash',
# [SGKK] Bleach 312v1 [720p/MKV]
'''
^(\[(?P<release_group>.+?)\][ ._-]*)? # Release Group and separator
(?P<series_name>.+?)[ ._-]+ # Show_Name and separator
(?P<ep_ab_num>\d{1,3}) # E01
(-(?P<extra_ab_ep_num>\d{1,3}))? # E02
(v(?P<version>[0-9]))? # version
[ ._-]+\[(?P<extra_info>\d{3,4}p) # Source_Quality_Etc-
(\[(?P<crc>\w{8})\])? # CRC
.*? # Separator and EOL
'''),
('anime_standard_codec',
# [Ayako]_Infinite_Stratos_-_IS_-_07_[H264][720p][EB7838FC]
# [Ayako] Infinite Stratos - IS - 07v2 [H264][720p][44419534]
# [Ayako-Shikkaku] Oniichan no Koto Nanka Zenzen Suki Janain Dakara ne - 10 [LQ][h264][720p] [8853B21C]
'''
^(\[(?P<release_group>.+?)\][ ._-]*)? # Release Group and separator
(?P<series_name>.+?)[ ._]* # Show_Name and separator
([ ._-]+-[ ._-]+[A-Z]+[ ._-]+)?[ ._-]+ # funny stuff, this is sooo nuts ! this will kick me in the butt one day
(?P<ep_ab_num>\d{1,3}) # E01
(-(?P<extra_ab_ep_num>\d{1,3}))? # E02
(v(?P<version>[0-9]))? # version
([ ._-](\[\w{1,2}\])?\[[a-z][.]?\w{2,4}\])? #codec
[ ._-]*\[(?P<extra_info>(\d{3,4}[xp]?\d{0,4})?[\.\w\s-]*)\] # Source_Quality_Etc-
(\[(?P<crc>\w{8})\])?
.*? # Separator and EOL
'''),
('anime_and_normal',
# Bleach - s16e03-04 - 313-314
# Bleach.s16e03-04.313-314
# Bleach s16e03e04 313-314
'''
^(?P<series_name>.+?)[ ._-]+ # start of string and series name and non optinal separator
[sS](?P<season_num>\d+)[. _-]* # S01 and optional separator
[eE](?P<ep_num>\d+) # epipisode E02
(([. _-]*e|-) # linking e/- char
(?P<extra_ep_num>\d+))* # additional E03/etc
([ ._-]{2,}|[ ._]+) # if "-" is used to separate at least something else has to be there(->{2,}) "s16e03-04-313-314" would make sens any way
(?P<ep_ab_num>\d{1,3}) # absolute number
(-(?P<extra_ab_ep_num>\d{1,3}))? # "-" as separator and anditional absolute number, all optinal
(v(?P<version>[0-9]))? # the version e.g. "v2"
.*?
'''
),
('anime_and_normal_x',
# Bleach - s16e03-04 - 313-314
# Bleach.s16e03-04.313-314
# Bleach s16e03e04 313-314
'''
^(?P<series_name>.+?)[ ._-]+ # start of string and series name and non optinal separator
(?P<season_num>\d+)[. _-]* # S01 and optional separator
[xX](?P<ep_num>\d+) # epipisode E02
(([. _-]*e|-) # linking e/- char
(?P<extra_ep_num>\d+))* # additional E03/etc
([ ._-]{2,}|[ ._]+) # if "-" is used to separate at least something else has to be there(->{2,}) "s16e03-04-313-314" would make sens any way
(?P<ep_ab_num>\d{1,3}) # absolute number
(-(?P<extra_ab_ep_num>\d{1,3}))? # "-" as separator and anditional absolute number, all optinal
(v(?P<version>[0-9]))? # the version e.g. "v2"
.*?
'''
),
('anime_and_normal_reverse',
# Bleach - 313-314 - s16e03-04
'''
^(?P<series_name>.+?)[ ._-]+ # start of string and series name and non optinal separator
(?P<ep_ab_num>\d{1,3}) # absolute number
(-(?P<extra_ab_ep_num>\d{1,3}))? # "-" as separator and anditional absolute number, all optinal
(v(?P<version>[0-9]))? # the version e.g. "v2"
([ ._-]{2,}|[ ._]+) # if "-" is used to separate at least something else has to be there(->{2,}) "s16e03-04-313-314" would make sens any way
[sS](?P<season_num>\d+)[. _-]* # S01 and optional separator
[eE](?P<ep_num>\d+) # epipisode E02
(([. _-]*e|-) # linking e/- char
(?P<extra_ep_num>\d+))* # additional E03/etc
.*?
'''
),
('anime_and_normal_front',
# 165.Naruto Shippuuden.s08e014
'''
^(?P<ep_ab_num>\d{1,3}) # start of string and absolute number
(-(?P<extra_ab_ep_num>\d{1,3}))? # "-" as separator and anditional absolute number, all optinal
(v(?P<version>[0-9]))?[ ._-]+ # the version e.g. "v2"
(?P<series_name>.+?)[ ._-]+
[sS](?P<season_num>\d+)[. _-]* # S01 and optional separator
[eE](?P<ep_num>\d+)
(([. _-]*e|-) # linking e/- char
(?P<extra_ep_num>\d+))* # additional E03/etc
.*?
'''
),
('anime_ep_name',
"""
^(?:\[(?P<release_group>.+?)\][ ._-]*)
(?P<series_name>.+?)[ ._-]+
(?P<ep_ab_num>\d{1,3})
(-(?P<extra_ab_ep_num>\d{1,3}))?[ ._-]*?
(?:v(?P<version>[0-9])[ ._-]+?)?
(?:.+?[ ._-]+?)?
\[(?P<extra_info>\w+)\][ ._-]?
(?:\[(?P<crc>\w{8})\])?
.*?
"""
),
('anime_bare',
# One Piece - 102
# [ACX]_Wolf's_Spirit_001.mkv
'''
^(\[(?P<release_group>.+?)\][ ._-]*)?
(?P<series_name>.+?)[ ._-]+ # Show_Name and separator
(?P<ep_ab_num>\d{3}) # E01
(-(?P<extra_ab_ep_num>\d{3}))? # E02
(v(?P<version>[0-9]))? # v2
.*? # Separator and EOL
''')
]}

View File

@ -53,13 +53,15 @@ class TVShow():
self.genre = "Comedy"
self.air_by_date = 0
self.sports = 0
self.anime = 0
class TVEpisode(tv.TVEpisode):
def __init__(self, season, episode, name):
def __init__(self, season, episode, absolute_number, name):
self.relatedEps = []
self._name = name
self._season = season
self._episode = episode
self._absolute_number = absolute_number
self._scene_season = season
self._scene_episode = episode
self._airdate = datetime.date(2010, 3, 9)
@ -173,9 +175,9 @@ def validate_name(pattern, multi=None, file_only=False, abd=False, sports=False)
return True
def _generate_sample_ep(multi=None, abd=False, sports=False):
def _generate_sample_ep(multi=None, abd=False, sports=False, anime=False):
# make a fake episode object
ep = TVEpisode(2, 3, "Ep Name")
ep = TVEpisode(2, 3, 3, "Ep Name")
ep._status = Quality.compositeStatus(DOWNLOADED, Quality.HDTV)
ep._airdate = datetime.date(2011, 3, 9)
@ -186,6 +188,9 @@ def _generate_sample_ep(multi=None, abd=False, sports=False):
elif sports:
ep._release_name = 'Show.Name.100.Fighter.vs.Fighter.HDTV.XviD-RLSGROUP'
ep.show.sports = 1
elif anime:
ep._release_name = 'Show.Name.S02E03.HDTV.XviD-RLSGROUP'
ep.show.anime = 1
else:
ep._release_name = 'Show.Name.S02E03.HDTV.XviD-RLSGROUP'
@ -193,11 +198,11 @@ def _generate_sample_ep(multi=None, abd=False, sports=False):
ep._name = "Ep Name (1)"
ep._release_name = 'Show.Name.S02E03E04E05.HDTV.XviD-RLSGROUP'
secondEp = TVEpisode(2, 4, "Ep Name (2)")
secondEp = TVEpisode(2, 4, 4, "Ep Name (2)")
secondEp._status = Quality.compositeStatus(DOWNLOADED, Quality.HDTV)
secondEp._release_name = ep._release_name
thirdEp = TVEpisode(2, 5, "Ep Name (3)")
thirdEp = TVEpisode(2, 5, 5, "Ep Name (3)")
thirdEp._status = Quality.compositeStatus(DOWNLOADED, Quality.HDTV)
thirdEp._release_name = ep._release_name
@ -207,7 +212,7 @@ def _generate_sample_ep(multi=None, abd=False, sports=False):
return ep
def test_name(pattern, multi=None, abd=False, sports=False):
ep = _generate_sample_ep(multi, abd, sports)
def test_name(pattern, multi=None, abd=False, sports=False, anime=False):
ep = _generate_sample_ep(multi, abd, sports, anime)
return {'name': ep.formatted_filename(pattern, multi), 'dir': ep.formatted_dir(pattern, multi)}

View File

@ -70,7 +70,7 @@ class BoxcarNotifier:
handle = urllib2.urlopen(req, data)
handle.close()
except urllib2.URLError, e:
except urllib2.HTTPError, e:
# if we get an error back that doesn't have an error code then who knows what's really happening
if not hasattr(e, 'code'):
logger.log("Boxcar notification failed." + ex(e), logger.ERROR)

View File

@ -63,7 +63,7 @@ class Boxcar2Notifier:
handle = urllib2.urlopen(req, data)
handle.close()
except urllib2.URLError, e:
except urllib2.HTTPError, e:
# if we get an error back that doesn't have an error code then who knows what's really happening
if not hasattr(e, 'code'):
logger.log("Boxcar2 notification failed." + ex(e), logger.ERROR)

View File

@ -68,7 +68,7 @@ class PushoverNotifier:
handle = urllib2.urlopen(req, data)
handle.close()
except urllib2.URLError, e:
except urllib2.HTTPError, e:
# if we get an error back that doesn't have an error code then who knows what's really happening
if not hasattr(e, 'code'):
logger.log("Pushover notification failed." + ex(e), logger.ERROR)
@ -127,10 +127,6 @@ class PushoverNotifier:
logger.log("Notification for Pushover not enabled, skipping this notification", logger.DEBUG)
return False
# if no userKey was given then use the one from the config
if not userKey:
userKey = sickbeard.PUSHOVER_USERKEY
logger.log("Sending notification for " + message, logger.DEBUG)
# self._sendPushover(message, title, userKey)

View File

@ -20,7 +20,7 @@ import os
import sickbeard
from urllib import urlencode
from urllib2 import Request, urlopen, URLError
from urllib2 import Request, urlopen, HTTPError
from sickbeard import logger
from sickbeard import encodingKludge as ek
@ -87,7 +87,7 @@ class pyTivoNotifier:
try:
response = urlopen(request) #@UnusedVariable
except URLError, e:
except HTTPError , e:
if hasattr(e, 'reason'):
logger.log(u"pyTivo notification: Error, failed to reach a server")
logger.log(u"'Error reason: " + e.reason)

View File

@ -21,15 +21,12 @@ from __future__ import with_statement
import glob
import os
import re
import shlex
import subprocess
import stat
import copy
import sickbeard
from sickbeard import db
from sickbeard import classes
from sickbeard import common
from sickbeard import exceptions
from sickbeard import helpers
@ -37,7 +34,6 @@ from sickbeard import history
from sickbeard import logger
from sickbeard import notifiers
from sickbeard import show_name_helpers
from sickbeard import scene_exceptions
from sickbeard import failed_history
from sickbeard import name_cache
@ -46,6 +42,7 @@ from sickbeard.exceptions import ex
from sickbeard.name_parser.parser import NameParser, InvalidNameException
from lib import adba
class PostProcessor(object):
"""
@ -483,7 +480,7 @@ class PostProcessor(object):
return to_return
# parse the name to break it into show name, season, and episode
np = NameParser(file)
np = NameParser(file, useIndexers=True)
parse_result = np.parse(name)
self._log(u"Parsed " + name + " into " + str(parse_result).decode('utf-8', 'xmlcharrefreplace'), logger.DEBUG)
@ -508,6 +505,72 @@ class PostProcessor(object):
self._finalize(parse_result)
return to_return
def _analyze_anidb(self, filePath):
# TODO: rewrite this
return (None, None, None)
if not helpers.set_up_anidb_connection():
return (None, None, None)
ep = self._build_anidb_episode(sickbeard.ADBA_CONNECTION, filePath)
try:
self._log(u"Trying to lookup " + str(filePath) + " on anidb", logger.MESSAGE)
ep.load_data()
except Exception, e:
self._log(u"exception msg: " + str(e))
raise InvalidNameException
else:
self.anidbEpisode = ep
#TODO: clean code. it looks like it's from hell
for name in ep.allNames:
indexer_id = name_cache.retrieveNameFromCache(name)
if not indexer_id:
show = helpers.get_show_by_name(name)
if show:
indexer_id = show.indexerid
else:
indexer_id = 0
if indexer_id:
name_cache.addNameToCache(name, indexer_id)
if indexer_id:
try:
show = helpers.findCertainShow(sickbeard.showList, indexer_id)
(season, episodes) = helpers.get_all_episodes_from_absolute_number(show, None, [ep.epno])
except exceptions.EpisodeNotFoundByAbsoluteNumberException:
self._log(str(indexer_id) + ": Indexer object absolute number " + str(
ep.epno) + " is incomplete, skipping this episode")
else:
if len(episodes):
self._log(u"Lookup successful from anidb. ", logger.DEBUG)
return (indexer_id, season, episodes)
if ep.anidb_file_name:
self._log(u"Lookup successful, using anidb filename " + str(ep.anidb_file_name), logger.DEBUG)
return self._analyze_name(ep.anidb_file_name)
raise InvalidNameException
def _build_anidb_episode(self, connection, filePath):
ep = adba.Episode(connection, filePath=filePath,
paramsF=["quality", "anidb_file_name", "crc32"],
paramsA=["epno", "english_name", "short_name_list", "other_name", "synonym_list"])
return ep
def _add_to_anidb_mylist(self, filePath):
if helpers.set_up_anidb_connection():
if not self.anidbEpisode: # seams like we could parse the name before, now lets build the anidb object
self.anidbEpisode = self._build_anidb_episode(sickbeard.ADBA_CONNECTION, filePath)
self._log(u"Adding the file to the anidb mylist", logger.DEBUG)
try:
self.anidbEpisode.add_to_mylist(status=1) # status = 1 sets the status of the file to "internal HDD"
except Exception, e:
self._log(u"exception msg: " + str(e))
def _find_info(self):
"""
For a given file try to find the showid, season, and episode.
@ -676,7 +739,7 @@ class PostProcessor(object):
if not cur_name:
continue
ep_quality = common.Quality.nameQuality(cur_name)
ep_quality = common.Quality.nameQuality(cur_name, ep_obj.show.is_anime)
self._log(
u"Looking up quality for name " + cur_name + u", got " + common.Quality.qualityStrings[ep_quality],
logger.DEBUG)
@ -941,6 +1004,10 @@ class PostProcessor(object):
new_base_name = None
new_file_name = self.file_name
# add to anidb
if ep_obj.show.is_anime and sickbeard.ANIDB_USE_MYLIST:
self._add_to_anidb_mylist(self.file_path)
try:
# move the episode and associated files to the show dir
if self.process_method == "copy":

View File

@ -171,10 +171,15 @@ class ProperFinder():
curProper.season = -1
curProper.episode = parse_result.air_date or parse_result.sports_event_date
else:
curProper.season = parse_result.season_number if parse_result.season_number != None else 1
curProper.episode = parse_result.episode_numbers[0]
if parse_result.is_anime:
logger.log(u"I am sorry '"+curProper.name+"' seams to be an anime proper seach is not yet suported", logger.DEBUG)
continue
curProper.episode = parse_result.ab_episode_numbers[0]
else:
curProper.season = parse_result.season_number if parse_result.season_number != None else 1
curProper.episode = parse_result.episode_numbers[0]
curProper.quality = Quality.nameQuality(curProper.name)
curProper.quality = Quality.nameQuality(curProper.name, parse_result.is_anime)
# for each show in our list
for curShow in sickbeard.showList:

View File

@ -31,7 +31,9 @@ __all__ = ['ezrss',
'iptorrents',
'omgwtfnzbs',
'nextgen',
'speedcd'
'speedcd',
'nyaatorrents',
'fanzub'
]
import sickbeard
@ -160,7 +162,7 @@ def makeTorrentRssProvider(configString):
def getDefaultNewznabProviders():
return 'SickRage Index|http://lolo.sickbeard.com/|0|5030,5040,5060|0|eponly|0!!!NZBs.org|https://nzbs.org/||5030,5040,5060,5070,5090|0|eponly|0!!!Usenet-Crawler|https://www.usenet-crawler.com/||5030,5040,5060|0|eponly|0'
return 'Sick Beard Index|http://lolo.sickbeard.com/|0|5030,5040|0|eponly|0!!!NZBs.org|https://nzbs.org/||5030,5040|0|eponly|0!!!Usenet-Crawler|https://www.usenet-crawler.com/||5030,5040|0|eponly|0'
def getProviderModule(name):

View File

@ -46,7 +46,7 @@ class DTTProvider(generic.TorrentProvider):
def imageName(self):
return 'dailytvtorrents.gif'
def getQuality(self, item):
def getQuality(self, item, anime=False):
url = item.enclosures[0].href
quality = Quality.sceneQuality(url)
return quality

View File

@ -53,7 +53,7 @@ class EZRSSProvider(generic.TorrentProvider):
def imageName(self):
return 'ezrss.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
filename = item.filename
quality = Quality.nameQuality(filename)

View File

@ -0,0 +1,151 @@
# 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 sickbeard
import generic
from sickbeard import classes, show_name_helpers, helpers
from sickbeard import exceptions, logger
from sickbeard.common import *
from sickbeard import tvcache
from lib.dateutil.parser import parse as parseDate
class Fanzub(generic.NZBProvider):
def __init__(self):
generic.NZBProvider.__init__(self, "Fanzub")
self.supportsBacklog = False
self.supportsAbsoluteNumbering = True
self.enabled = False
self.cache = FanzubCache(self)
self.url = 'http://fanzub.com/'
def isEnabled(self):
return self.enabled
def imageName(self):
return 'fanzub.gif'
def _checkAuth(self):
return True
def _get_season_search_strings(self, ep_obj):
return [x for x in show_name_helpers.makeSceneSeasonSearchString(self.show, ep_obj)]
def _get_episode_search_strings(self, ep_obj, add_string=''):
return [x for x in show_name_helpers.makeSceneSearchString(self.show, ep_obj)]
def _doSearch(self, search_string, epcount=0, age=0):
if self.show and not self.show.is_anime:
logger.log(u"" + str(self.show.name) + " is not an anime skiping ...")
return []
params = {
"cat": "anime",
"q": search_string.encode('utf-8'),
"max": "100"
}
search_url = self.url + "rss?" + urllib.urlencode(params)
logger.log(u"Search url: " + search_url, logger.DEBUG)
data = self.cache.getRSSFeed(search_url)
if not data:
return []
if 'entries' in data:
items = data.entries
results = []
for curItem in items:
(title, url) = self._get_title_and_url(curItem)
if title and url:
results.append(curItem)
else:
logger.log(
u"The data returned from the " + self.name + " is incomplete, this result is unusable",
logger.DEBUG)
return results
return []
def findPropers(self, date=None):
results = []
for i in [2, 3, 4]: # we will look for a version 2, 3 and 4
"""
because of this the proper search failed !!
well more precisly because _doSearch does not accept a dict rather then a string
params = {
"q":"v"+str(i).encode('utf-8')
}
"""
for curResult in self._doSearch("v" + str(i)):
match = re.search('(\w{3}, \d{1,2} \w{3} \d{4} \d\d:\d\d:\d\d) [\+\-]\d{4}', curResult.findtext('pubDate'))
if not match:
continue
dateString = match.group(1)
resultDate = parseDate(dateString).replace(tzinfo=None)
if date == None or resultDate > date:
results.append(classes.Proper(curResult.findtext('title'), curResult.findtext('link'), resultDate))
return results
class FanzubCache(tvcache.TVCache):
def __init__(self, provider):
tvcache.TVCache.__init__(self, provider)
# only poll Fanzub every 20 minutes max
# we get 100 post each call !
self.minTime = 20
def _getRSSData(self):
params = {"cat": "anime".encode('utf-8'),
"max": "100".encode('utf-8')
}
rss_url = self.provider.url + 'rss?' + urllib.urlencode(params)
logger.log(self.provider.name + u" cache update URL: " + rss_url, logger.DEBUG)
return self.getRSSFeed(rss_url)
def _checkAuth(self, data):
return self.provider._checkAuthFromData(data)
provider = Fanzub()

View File

@ -54,6 +54,7 @@ class GenericProvider:
self.show = None
self.supportsBacklog = False
self.supportsAbsoluteNumbering = False
self.search_mode = None
self.search_fallback = False
@ -188,7 +189,7 @@ class GenericProvider:
def searchRSS(self, episodes):
return self.cache.findNeededEpisodes(episodes)
def getQuality(self, item):
def getQuality(self, item, anime=False):
"""
Figures out the quality of the given RSS item node
@ -197,7 +198,7 @@ class GenericProvider:
Returns a Quality value obtained from the node's data
"""
(title, url) = self._get_title_and_url(item) # @UnusedVariable
quality = Quality.sceneQuality(title)
quality = Quality.sceneQuality(title, anime)
return quality
def _doSearch(self, search_params, epcount=0, age=0):
@ -282,16 +283,16 @@ class GenericProvider:
(title, url) = self._get_title_and_url(item)
quality = self.getQuality(item)
# parse the file name
try:
myParser = NameParser(False)
myParser = NameParser(False, show=show, useIndexers=manualSearch)
parse_result = myParser.parse(title)
except InvalidNameException:
logger.log(u"Unable to parse the filename " + title + " into a valid episode", logger.WARNING)
continue
quality = self.getQuality(item, parse_result.is_anime)
# scene -> indexer numbering
parse_result = parse_result.convert(self.show)

View File

@ -79,7 +79,7 @@ class HDTorrentsProvider(generic.TorrentProvider):
def imageName(self):
return 'hdtorrents.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
return quality

View File

@ -71,9 +71,9 @@ class IPTorrentsProvider(generic.TorrentProvider):
def imageName(self):
return 'iptorrents.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _doLogin(self):

View File

@ -74,9 +74,9 @@ class KATProvider(generic.TorrentProvider):
def imageName(self):
return 'kat.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _reverseQuality(self, quality):

View File

@ -76,7 +76,7 @@ class NewzbinProvider(generic.NZBProvider):
def isEnabled(self):
return sickbeard.NEWZBIN
def getQuality(self, item):
def getQuality(self, item, anime=False):
attributes = item.report[0]
attr_dict = {}

View File

@ -40,7 +40,7 @@ from sickbeard.exceptions import ex, AuthException
class NewznabProvider(generic.NZBProvider):
def __init__(self, name, url, key='', catIDs='5030,5040,5060', search_mode='eponly', search_fallback=False):
def __init__(self, name, url, key='', catIDs='5030,5040', search_mode='eponly', search_fallback=False):
generic.NZBProvider.__init__(self, name)
@ -62,7 +62,7 @@ class NewznabProvider(generic.NZBProvider):
if catIDs:
self.catIDs = catIDs
else:
self.catIDs = '5030,5040, 5060'
self.catIDs = '5030,5040'
self.enabled = True
self.supportsBacklog = True
@ -86,7 +86,8 @@ class NewznabProvider(generic.NZBProvider):
to_return = []
# add new query strings for exceptions
name_exceptions = scene_exceptions.get_scene_exceptions(self.show.indexerid) + [self.show.name]
name_exceptions = scene_exceptions.get_scene_exceptions(self.show.indexerid, ep_obj.season) + [self.show.name]
name_exceptions = set(name_exceptions)
for cur_exception in name_exceptions:
cur_params = {}
@ -95,7 +96,7 @@ class NewznabProvider(generic.NZBProvider):
if ep_obj.show.indexer == 2:
cur_params['rid'] = ep_obj.show.indexerid
else:
cur_params['q'] = helpers.sanitizeSceneName(cur_exception)
cur_params['q'] = helpers.sanitizeSceneName(cur_exception).replace('.', '_')
# season
if ep_obj.show.air_by_date or ep_obj.show.sports:
@ -124,7 +125,7 @@ class NewznabProvider(generic.NZBProvider):
if ep_obj.show.indexer == 2:
params['rid'] = ep_obj.show.indexerid
else:
params['q'] = helpers.sanitizeSceneName(self.show.name)
params['q'] = helpers.sanitizeSceneName(self.show.name).replace('.', '_')
if self.show.air_by_date or self.show.sports:
date_str = str(ep_obj.airdate)
@ -148,7 +149,7 @@ class NewznabProvider(generic.NZBProvider):
continue
cur_return = params.copy()
cur_return['q'] = helpers.sanitizeSceneName(cur_exception)
cur_return['q'] = helpers.sanitizeSceneName(cur_exception).replace('.', '_')
to_return.append(cur_return)
return to_return
@ -185,6 +186,12 @@ class NewznabProvider(generic.NZBProvider):
"limit": 100,
"cat": self.catIDs}
# sports and anime catIDs
if self.show.is_sports:
params['cat'] += ',5060'
elif self.show.is_anime:
params['cat'] += ',5070,5090'
# if max_age is set, use it, don't allow it to be missing
if age or not params['maxage']:
params['maxage'] = age
@ -288,6 +295,12 @@ class NewznabCache(tvcache.TVCache):
params = {"t": "tvsearch",
"cat": self.provider.catIDs}
# sports catIDs
params['cat'] += ',5060'
# anime catIDs
params['cat'] += ',5070,5090'
if self.provider.needs_auth and self.provider.key:
params['apikey'] = self.provider.key

View File

@ -77,9 +77,9 @@ class NextGenProvider(generic.TorrentProvider):
def imageName(self):
return 'nextgen.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def getLoginParams(self):

View File

@ -36,14 +36,14 @@ class NyaaProvider(generic.TorrentProvider):
generic.TorrentProvider.__init__(self, "NyaaTorrents")
self.supportsBacklog = True
self.supportsAbsoluteNumbering = True
self.enabled = False
self.ratio = None
self.cache = NyaaCache(self)
self.url = 'http://www.nyaa.eu/'
self.url = 'http://www.nyaa.se/'
def isEnabled(self):
return self.enabled
@ -68,6 +68,9 @@ class NyaaProvider(generic.TorrentProvider):
return self._get_season_search_strings(ep_obj)
def _doSearch(self, search_string, show=None, age=None):
if self.show and not self.show.is_anime:
logger.log(u"" + str(self.show.name) + " is not an anime skiping " + str(self.name))
return []
params = {"term": search_string.encode('utf-8'),
"sort": '2', #Sort Descending By Seeders
@ -78,7 +81,7 @@ class NyaaProvider(generic.TorrentProvider):
logger.log(u"Search string: " + searchURL, logger.DEBUG)
data = self.getURL(searchURL)
data = self.cache.getRSSFeed(searchURL)
if not data:
logger.log(u"Error trying to load NyaaTorrents RSS feed: " + searchURL, logger.ERROR)
@ -115,6 +118,8 @@ class NyaaProvider(generic.TorrentProvider):
return match.group(1)
return None
def seedRatio(self):
return self.ratio
class NyaaCache(tvcache.TVCache):
def __init__(self, provider):

View File

@ -97,7 +97,7 @@ class OmgwtfnzbsProvider(generic.NZBProvider):
def _get_title_and_url(self, item):
return (item['release'], item['getnzb'])
def _doSearch(self, search, show=None, retention=0):
def _doSearch(self, search, epcount=0, retention=0):
self._checkAuth()

View File

@ -73,9 +73,9 @@ class PublicHDProvider(generic.TorrentProvider):
def imageName(self):
return 'publichd.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _get_season_search_strings(self, ep_obj):

View File

@ -79,9 +79,9 @@ class SCCProvider(generic.TorrentProvider):
def imageName(self):
return 'scc.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _doLogin(self):

View File

@ -72,9 +72,9 @@ class SpeedCDProvider(generic.TorrentProvider):
def imageName(self):
return 'speedcd.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _doLogin(self):

View File

@ -73,9 +73,9 @@ class ThePirateBayProvider(generic.TorrentProvider):
def imageName(self):
return 'thepiratebay.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _reverseQuality(self, quality):

View File

@ -78,9 +78,9 @@ class TorrentDayProvider(generic.TorrentProvider):
def imageName(self):
return 'torrentday.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _doLogin(self):

View File

@ -73,9 +73,9 @@ class TorrentLeechProvider(generic.TorrentProvider):
def imageName(self):
return 'torrentleech.png'
def getQuality(self, item):
def getQuality(self, item, anime=False):
quality = Quality.sceneQuality(item[0])
quality = Quality.sceneQuality(item[0], anime)
return quality
def _doLogin(self):

View File

@ -19,23 +19,66 @@
import re
import sickbeard
from lib import adba
from sickbeard import helpers
from sickbeard import name_cache
from sickbeard import logger
from sickbeard import db
exceptionCache = {}
exceptionSeasonCache = {}
exceptionIndexerCache = {}
def get_scene_exceptions(indexer_id):
def get_scene_exceptions(indexer_id, season=-1):
"""
Given a indexer_id, return a list of all the scene exceptions.
"""
myDB = db.DBConnection("cache.db")
exceptions = myDB.select("SELECT show_name FROM scene_exceptions WHERE indexer_id = ?", [indexer_id])
return [cur_exception["show_name"] for cur_exception in exceptions]
global exceptionCache
if indexer_id not in exceptionCache or season not in exceptionCache[indexer_id]:
myDB = db.DBConnection("cache.db")
exceptions = myDB.select("SELECT show_name FROM scene_exceptions WHERE indexer_id = ? and season = ?", [indexer_id, season])
exceptionsList = [cur_exception["show_name"] for cur_exception in exceptions]
if not indexer_id in exceptionCache:
exceptionCache[indexer_id] = {}
exceptionCache[indexer_id][season] = exceptionsList
else:
exceptionsList = exceptionCache[indexer_id][season]
if season == 1: # if we where looking for season 1 we can add generic names
exceptionsList += get_scene_exceptions(indexer_id, season=-1)
return exceptionsList
def get_all_scene_exceptions(indexer_id):
myDB = db.DBConnection("cache.db")
exceptions = myDB.select("SELECT show_name,season FROM scene_exceptions WHERE indexer_id = ?", [indexer_id])
exceptionsList = {}
[cur_exception["show_name"] for cur_exception in exceptions]
for cur_exception in exceptions:
if not cur_exception["season"] in exceptionsList:
exceptionsList[cur_exception["season"]] = []
exceptionsList[cur_exception["season"]].append(cur_exception["show_name"])
return exceptionsList
def get_scene_seasons(indexer_id):
"""
return a list of season numbers that have scene exceptions
"""
global exceptionSeasonCache
if indexer_id not in exceptionSeasonCache:
myDB = db.DBConnection("cache.db")
sqlResults = myDB.select("SELECT DISTINCT(season) as season FROM scene_exceptions WHERE indexer_id = ?", [indexer_id])
exceptionSeasonCache[indexer_id] = [int(x["season"]) for x in sqlResults]
return exceptionSeasonCache[indexer_id]
def get_scene_exception_by_name(show_name):
return get_scene_exception_by_name_multiple(show_name)[0]
def get_scene_exception_by_name_multiple(show_name):
"""
Given a show name, return the indexerid of the exception, None if no exception
is present.
@ -44,24 +87,25 @@ def get_scene_exception_by_name(show_name):
myDB = db.DBConnection("cache.db")
# try the obvious case first
exception_result = myDB.select("SELECT indexer_id FROM scene_exceptions WHERE LOWER(show_name) = ?",
[show_name.lower()])
exception_result = myDB.select("SELECT indexer_id, season FROM scene_exceptions WHERE LOWER(show_name) = ? ORDER BY season ASC", [show_name.lower()])
if exception_result:
return int(exception_result[0]["indexer_id"])
return [(int(x["indexer_id"]), int(x["season"])) for x in exception_result]
all_exception_results = myDB.select("SELECT show_name, indexer_id FROM scene_exceptions")
out = []
all_exception_results = myDB.select("SELECT show_name, indexer_id, season FROM scene_exceptions")
for cur_exception in all_exception_results:
cur_exception_name = cur_exception["show_name"]
cur_indexer_id = int(cur_exception["indexer_id"])
cur_season = int(cur_exception["season"])
if show_name.lower() in (
cur_exception_name.lower(), helpers.sanitizeSceneName(cur_exception_name).lower().replace('.', ' ')):
logger.log(u"Scene exception lookup got Indexer ID " + str(cur_indexer_id) + u", using that", logger.DEBUG)
return cur_indexer_id
return None
if show_name.lower() in (cur_exception_name.lower(), sickbeard.helpers.sanitizeSceneName(cur_exception_name).lower().replace('.', ' ')):
logger.log(u"Scene exception lookup got indexer id " + str(cur_indexer_id) + u", using that", logger.DEBUG)
out.append((cur_indexer_id, cur_season))
if out:
return out
else:
return [(None, None)]
def retrieve_exceptions():
"""
@ -69,6 +113,8 @@ def retrieve_exceptions():
scene_exceptions table in cache.db. Also clears the scene name cache.
"""
global exceptionCache, exceptionSeasonCache
exception_dict = {}
# exceptions are stored on github pages
@ -97,10 +143,27 @@ def retrieve_exceptions():
indexer_id = int(indexer_id)
# regex out the list of shows, taking \' into account
alias_list = [re.sub(r'\\(.)', r'\1', x) for x in re.findall(r"'(.*?)(?<!\\)',?", aliases)]
#alias_list = [re.sub(r'\\(.)', r'\1', x) for x in re.findall(r"'(.*?)(?<!\\)',?", aliases)]
alias_list = [{re.sub(r'\\(.)', r'\1', x): -1} for x in re.findall(r"'(.*?)(?<!\\)',?", aliases)]
exception_dict[indexer_id] = alias_list
logger.log(u"Checking for XEM scene exception updates for " + sickbeard.indexerApi(indexer).name)
xem_exceptions = _xem_excpetions_fetcher(indexer)
for xem_ex in xem_exceptions: # anidb xml anime exceptions
if xem_ex in exception_dict:
exception_dict[xem_ex] = exception_dict[xem_ex] + xem_exceptions[xem_ex]
else:
exception_dict[xem_ex] = xem_exceptions[xem_ex]
logger.log(u"Checking for scene exception updates for AniDB")
local_exceptions = _retrieve_anidb_mainnames()
for local_ex in local_exceptions: # anidb xml anime exceptions
if local_ex in exception_dict:
exception_dict[local_ex] = exception_dict[local_ex] + local_exceptions[local_ex]
else:
exception_dict[local_ex] = local_exceptions[local_ex]
myDB = db.DBConnection("cache.db")
changed_exceptions = False
@ -112,20 +175,25 @@ def retrieve_exceptions():
existing_exceptions = [x["show_name"] for x in
myDB.select("SELECT * FROM scene_exceptions WHERE indexer_id = ?", [cur_indexer_id])]
for cur_exception in exception_dict[cur_indexer_id]:
for cur_exception_dict in exception_dict[cur_indexer_id]:
cur_exception, curSeason = cur_exception_dict.items()[0]
# if this exception isn't already in the DB then add it
if cur_exception not in existing_exceptions:
myDB.action("INSERT INTO scene_exceptions (indexer_id, show_name) VALUES (?,?)",
[cur_indexer_id, cur_exception])
myDB.action("INSERT INTO scene_exceptions (indexer_id, show_name, season) VALUES (?,?,?)",
[cur_indexer_id, cur_exception, curSeason])
changed_exceptions = True
# since this could invalidate the results of the cache we clear it out after updating
if changed_exceptions:
logger.log(u"Updated scene exceptions")
name_cache.clearCache()
exceptionCache = {}
exceptionSeasonCache = {}
else:
logger.log(u"No scene exceptions update needed")
# update indexer cache
updateIndexerCache()
def update_scene_exceptions(indexer_id, scene_exceptions):
"""
@ -139,4 +207,65 @@ def update_scene_exceptions(indexer_id, scene_exceptions):
for cur_exception in scene_exceptions:
myDB.action("INSERT INTO scene_exceptions (indexer_id, show_name) VALUES (?,?)", [indexer_id, cur_exception])
name_cache.clearCache()
name_cache.clearCache()
def updateIndexerCache():
logger.log(u"Updating internal scene name cache", logger.MESSAGE)
_excpetionDots = []
global exceptionIndexerCache
exceptionIndexerCache = {}
for show in sickbeard.showList:
for curSeason in [-1] + sickbeard.scene_exceptions.get_scene_seasons(show.indexerid):
exceptionIndexerCache[helpers.full_sanitizeSceneName(show.name)] = show.indexerid
_excpetionDots.append(".")
for name in get_scene_exceptions(show.indexerid, season=curSeason):
exceptionIndexerCache[name] = show.indexerid
exceptionIndexerCache[helpers.full_sanitizeSceneName(name)] = show.indexerid
_excpetionDots.append(".")
logger.log(u"Updated internal scene name cache " + "".join(_excpetionDots), logger.MESSAGE)
logger.log(u"Internal scene name cache set to: " + str(exceptionIndexerCache), logger.DEBUG)
def _retrieve_anidb_mainnames():
anidb_mainNames = {}
for show in sickbeard.showList:
if show.is_anime and show.indexer == 1:
try:
anime = adba.Anime(None, name=show.name, tvdbid=show.indexerid, autoCorrectName=True)
except:
continue
else:
if anime.name and anime.name != show.name:
anidb_mainNames[show.indexerid] = [{anime.name:-1}]
#logger.log("anidb anime names: " + str(anidb_mainNames), logger.DEBUG)
return anidb_mainNames
def _xem_excpetions_fetcher(indexer):
exception_dict = {}
url = "http://thexem.de/map/allNames?origin=%s&seasonNumbers=1" % sickbeard.indexerApi(indexer).config['xem_origin']
url_data = helpers.getURL(url, json=True)
if url_data is None:
logger.log(u"Check scene exceptions update failed. Unable to get URL: " + url, logger.ERROR)
return exception_dict
if url_data['result'] == 'failure':
return exception_dict
for indexerid, names in url_data['data'].items():
exception_dict[int(indexerid)] = names
#logger.log(u"xem exception dict: " + str(exception_dict), logger.DEBUG)
return exception_dict
def getSceneSeasons(indexer_id):
"""get a list of season numbers that have scene excpetions
"""
myDB = db.DBConnection("cache.db")
seasons = myDB.select("SELECT DISTINCT season FROM scene_exceptions WHERE indexer_id = ?", [indexer_id])
return [cur_exception["season"] for cur_exception in seasons]

View File

@ -39,7 +39,7 @@ from lib import requests
MAX_XEM_AGE_SECS = 86400 # 1 day
def get_scene_numbering(indexer_id, indexer, season, episode, fallback_to_xem=True):
def get_scene_numbering(indexer_id, indexer, season, episode, absolute_number=None, fallback_to_xem=True):
"""
Returns a tuple, (season, episode), with the scene numbering (if there is one),
otherwise returns the xem numbering (if fallback_to_xem is set), otherwise
@ -53,28 +53,28 @@ def get_scene_numbering(indexer_id, indexer, season, episode, fallback_to_xem=Tr
@return: (int, int) a tuple with (season, episode)
"""
if indexer_id is None or season is None or episode is None:
return (season, episode)
return (season, episode, absolute_number)
indexer_id = int(indexer_id)
indexer = int(indexer)
result = find_scene_numbering(indexer_id, indexer, season, episode)
result = find_scene_numbering(indexer_id, indexer, season, episode, absolute_number)
if result:
return result
else:
if fallback_to_xem:
xem_result = find_xem_numbering(indexer_id, indexer, season, episode)
xem_result = find_xem_numbering(indexer_id, indexer, season, episode, absolute_number)
if xem_result:
return xem_result
return (season, episode)
return (season, episode, absolute_number)
def find_scene_numbering(indexer_id, indexer, season, episode):
def find_scene_numbering(indexer_id, indexer, season, episode, absolute_number=None):
"""
Same as get_scene_numbering(), but returns None if scene numbering is not set
"""
if indexer_id is None or season is None or episode is None:
return (season, episode)
return (season, episode, absolute_number)
indexer_id = int(indexer_id)
indexer = int(indexer)
@ -82,13 +82,13 @@ def find_scene_numbering(indexer_id, indexer, season, episode):
myDB = db.DBConnection()
rows = myDB.select(
"SELECT scene_season, scene_episode FROM scene_numbering WHERE indexer = ? and indexer_id = ? and season = ? and episode = ?",
"SELECT scene_season, scene_episode, scene_absolute_number FROM scene_numbering WHERE indexer = ? and indexer_id = ? and season = ? and episode = ?",
[indexer, indexer_id, season, episode])
if rows:
return (int(rows[0]["scene_season"]), int(rows[0]["scene_episode"]))
return (int(rows[0]["scene_season"]), int(rows[0]["scene_episode"]), int(rows[0]["scene_absolute_number"]))
def get_indexer_numbering(indexer_id, indexer, sceneSeason, sceneEpisode, fallback_to_xem=True):
def get_indexer_numbering(indexer_id, indexer, sceneSeason, sceneEpisode, sceneAbsoluteNumber=None, fallback_to_xem=True):
"""
Returns a tuple, (season, episode) with the TVDB and TVRAGE numbering for (sceneSeason, sceneEpisode)
(this works like the reverse of get_scene_numbering)
@ -102,14 +102,14 @@ def get_indexer_numbering(indexer_id, indexer, sceneSeason, sceneEpisode, fallba
myDB = db.DBConnection()
rows = myDB.select(
"SELECT season, episode FROM scene_numbering WHERE indexer = ? and indexer_id = ? and scene_season = ? and scene_episode = ?",
"SELECT season, episode, absolute_number FROM scene_numbering WHERE indexer = ? and indexer_id = ? and scene_season = ? and scene_episode = ?",
[indexer, indexer_id, sceneSeason, sceneEpisode])
if rows:
return (int(rows[0]["season"]), int(rows[0]["episode"]))
return (int(rows[0]["season"]), int(rows[0]["episode"]), int(rows[0]["absolute_number"]))
else:
if fallback_to_xem:
return get_indexer_numbering_for_xem(indexer_id, indexer, sceneSeason, sceneEpisode)
return (sceneSeason, sceneEpisode)
return get_indexer_numbering_for_xem(indexer_id, indexer, sceneSeason, sceneEpisode, sceneAbsoluteNumber)
return (sceneSeason, sceneEpisode, sceneAbsoluteNumber)
def get_scene_numbering_for_show(indexer_id, indexer):
@ -127,16 +127,27 @@ def get_scene_numbering_for_show(indexer_id, indexer):
myDB = db.DBConnection()
rows = myDB.select(
'SELECT season, episode, scene_season, scene_episode FROM scene_numbering WHERE indexer = ? and indexer_id = ? ORDER BY season, episode',
'SELECT season, episode, absolute_number, scene_season, scene_episode, scene_absolute_number FROM scene_numbering WHERE indexer = ? and indexer_id = ? ORDER BY season, episode',
[indexer, indexer_id])
result = {}
for row in rows:
result[(int(row['season']), int(row['episode']))] = (int(row['scene_season']), int(row['scene_episode']))
season = int(row['season'])
episode = int(row['episode'])
scene_season = int(row['scene_season'])
scene_episode = int(row['scene_episode'])
scene_absolute_number = int(row['scene_absolute_number'])
try:
result[(season, episode)]
except:
result[(season, episode)] = (scene_season, scene_episode, scene_absolute_number)
return result
def set_scene_numbering(indexer_id, indexer, season, episode, sceneSeason=None, sceneEpisode=None):
def set_scene_numbering(indexer_id, indexer, season, episode, absolute_number, sceneSeason=None, sceneEpisode=None,
sceneAbsoluteNumber=None):
"""
Set scene numbering for a season/episode.
To clear the scene numbering, leave both sceneSeason and sceneEpisode as None.
@ -151,7 +162,7 @@ def set_scene_numbering(indexer_id, indexer, season, episode, sceneSeason=None,
myDB = db.DBConnection()
# sanity
#if sceneSeason == None: sceneSeason = season
# if sceneSeason == None: sceneSeason = season
#if sceneEpisode == None: sceneEpisode = episode
# delete any existing record first
@ -161,11 +172,11 @@ def set_scene_numbering(indexer_id, indexer, season, episode, sceneSeason=None,
# now, if the new numbering is not the default, we save a new record
if sceneSeason is not None and sceneEpisode is not None:
myDB.action(
"INSERT INTO scene_numbering (indexer, indexer_id, season, episode, scene_season, scene_episode) VALUES (?,?,?,?,?,?)",
[indexer, indexer_id, season, episode, sceneSeason, sceneEpisode])
"INSERT INTO scene_numbering (indexer, indexer_id, season, episode, absolute_number, scene_season, scene_episode, scene_absolute_number) VALUES (?,?,?,?,?,?,?,?)",
[indexer, indexer_id, season, episode, absolute_number, sceneSeason, sceneEpisode, sceneAbsoluteNumber])
def find_xem_numbering(indexer_id, indexer, season, episode):
def find_xem_numbering(indexer_id, indexer, season, episode, absolute_number):
"""
Returns the scene numbering, as retrieved from xem.
Refreshes/Loads as needed.
@ -176,7 +187,7 @@ def find_xem_numbering(indexer_id, indexer, season, episode):
@return: (int, int) a tuple of scene_season, scene_episode, or None if there is no special mapping.
"""
if indexer_id is None or season is None or episode is None:
return None
return (season, episode, absolute_number)
indexer_id = int(indexer_id)
indexer = int(indexer)
@ -185,29 +196,23 @@ def find_xem_numbering(indexer_id, indexer, season, episode):
_xem_refresh(indexer_id, indexer)
cacheDB = db.DBConnection('cache.db')
myDB = db.DBConnection()
rows = cacheDB.select(
"SELECT scene_season, scene_episode FROM xem_numbering WHERE indexer = ? and indexer_id = ? and season = ? and episode = ?",
"SELECT scene_season, scene_episode, scene_absolute_number FROM xem_numbering WHERE indexer = ? and indexer_id = ? and season = ? and episode = ?",
[indexer, indexer_id, season, episode])
scene_seasons = cacheDB.select(
"SELECT COUNT(DISTINCT scene_season) as count FROM xem_numbering WHERE indexer = ? and indexer_id = ?",
[indexer, indexer_id])
indexer_seasons = myDB.select(
"SELECT COUNT(DISTINCT season) as count FROM tv_episodes WHERE indexer = ? and showid = ?",
[indexer, indexer_id])
if rows:
return (int(rows[0]["scene_season"]), int(rows[0]["scene_episode"]))
elif int(scene_seasons[0]["count"]) > 0 and int(indexer_seasons[0]["count"]) > int(scene_seasons[0]["count"]):
return (0,0)
return (int(rows[0]["scene_season"]), int(rows[0]["scene_episode"]), int(rows[0]["scene_absolute_number"]))
elif cacheDB.select(
"SELECT * FROM xem_numbering WHERE indexer = ? and indexer_id = ?",
[indexer, indexer_id]):
return (0, 0, 0)
else:
return None
def get_indexer_numbering_for_xem(indexer_id, indexer, sceneSeason, sceneEpisode):
def get_indexer_numbering_for_xem(indexer_id, indexer, sceneSeason, sceneEpisode, sceneAbsoluteNumber):
"""
Reverse of find_xem_numbering: lookup a tvdb season and episode using scene numbering
@ -226,12 +231,12 @@ def get_indexer_numbering_for_xem(indexer_id, indexer, sceneSeason, sceneEpisode
_xem_refresh(indexer_id, indexer)
cacheDB = db.DBConnection('cache.db')
rows = cacheDB.select(
"SELECT season, episode FROM xem_numbering WHERE indexer = ? and indexer_id = ? and scene_season = ? and scene_episode = ?",
"SELECT season, episode, absolute_number FROM xem_numbering WHERE indexer = ? and indexer_id = ? and scene_season = ? and scene_episode = ?",
[indexer, indexer_id, sceneSeason, sceneEpisode])
if rows:
return (int(rows[0]["season"]), int(rows[0]["episode"]))
return (int(rows[0]["season"]), int(rows[0]["episode"]), int(rows[0]["absolute_number"]))
else:
return (sceneSeason, sceneEpisode)
return (sceneSeason, sceneEpisode, sceneAbsoluteNumber)
def _xem_refresh_needed(indexer_id, indexer):
@ -286,27 +291,35 @@ def _xem_refresh(indexer_id, indexer):
return None
result = data
ql = []
cacheDB = db.DBConnection('cache.db')
ql = []
if result:
ql.append(["INSERT OR REPLACE INTO xem_refresh (indexer, indexer_id, last_refreshed) VALUES (?,?,?)",
[indexer, indexer_id, time.time()]])
[indexer, indexer_id, time.time()]])
if 'success' in result['result']:
ql.append(["DELETE FROM xem_numbering where indexer = ? and indexer_id = ?", [indexer, indexer_id]])
for entry in result['data']:
if 'scene' in entry:
ql.append([
"INSERT INTO xem_numbering (indexer, indexer_id, season, episode, scene_season, scene_episode) VALUES (?,?,?,?,?,?)",
[indexer, indexer_id, entry[sickbeard.indexerApi(indexer).config['xem_origin']]['season'],
"INSERT OR IGNORE INTO xem_numbering (indexer, indexer_id, season, episode, absolute_number, scene_season, scene_episode, scene_absolute_number) VALUES (?,?,?,?,?,?,?,?)",
[indexer, indexer_id,
entry[sickbeard.indexerApi(indexer).config['xem_origin']]['season'],
entry[sickbeard.indexerApi(indexer).config['xem_origin']]['episode'],
entry['scene']['season'], entry['scene']['episode']]])
entry[sickbeard.indexerApi(indexer).config['xem_origin']]['absolute'],
entry['scene']['season'],
entry['scene']['episode'],
entry['scene']['absolute']]])
if 'scene_2' in entry: # for doubles
ql.append([
"INSERT INTO xem_numbering (indexer, indexer_id, season, episode, scene_season, scene_episode) VALUES (?,?,?,?,?,?)",
[indexer, indexer_id, entry[sickbeard.indexerApi(indexer).config['xem_origin']]['season'],
"INSERT OR IGNORE INTO xem_numbering (indexer, indexer_id, season, episode, absolute_number, scene_season, scene_episode, scene_absolute_number) VALUES (?,?,?,?,?,?,?,?)",
[indexer, indexer_id,
entry[sickbeard.indexerApi(indexer).config['xem_origin']]['season'],
entry[sickbeard.indexerApi(indexer).config['xem_origin']]['episode'],
entry['scene_2']['season'], entry['scene_2']['episode']]])
entry[sickbeard.indexerApi(indexer).config['xem_origin']]['absolute'],
entry['scene_2']['season'],
entry['scene_2']['episode'],
entry['scene_2']['absolute']]])
else:
logger.log(u'Failed to get XEM scene data for show %s from %s because "%s"' % (
indexer_id, sickbeard.indexerApi(indexer).name, result['message']), logger.DEBUG)
@ -322,6 +335,7 @@ def _xem_refresh(indexer_id, indexer):
if ql:
cacheDB.mass_action(ql)
def get_xem_numbering_for_show(indexer_id, indexer):
"""
Returns a dict of (season, episode) : (sceneSeason, sceneEpisode) mappings
@ -340,12 +354,18 @@ def get_xem_numbering_for_show(indexer_id, indexer):
cacheDB = db.DBConnection('cache.db')
rows = cacheDB.select(
'SELECT season, episode, scene_season, scene_episode FROM xem_numbering WHERE indexer = ? and indexer_id = ? ORDER BY season, episode',
'SELECT season, episode, absolute_number, scene_season, scene_episode, scene_absolute_number FROM xem_numbering WHERE indexer = ? and indexer_id = ? ORDER BY season, episode',
[indexer, indexer_id])
result = {}
for row in rows:
result[(int(row['season']), int(row['episode']))] = (int(row['scene_season']), int(row['scene_episode']))
season = int(row['season'])
episode = int(row['episode'])
scene_season = int(row['scene_season'])
scene_episode = int(row['scene_episode'])
scene_absolute_number = int(row['scene_absolute_number'])
result[(season, episode)] = (scene_season, scene_episode, scene_absolute_number)
return result
@ -368,7 +388,8 @@ def get_xem_numbering_for_season(indexer_id, indexer, season):
cacheDB = db.DBConnection('cache.db')
rows = cacheDB.select(
'SELECT season, scene_season FROM xem_numbering WHERE indexer = ? and indexer_id = ? AND season = ? ORDER BY season', [indexer, indexer_id, season])
'SELECT season, scene_season FROM xem_numbering WHERE indexer = ? and indexer_id = ? AND season = ? ORDER BY season',
[indexer, indexer_id, season])
result = {}
if rows:
@ -389,25 +410,29 @@ def fix_scene_numbering():
"SELECT showid, indexerid, indexer, episode_id, season, episode FROM tv_episodes WHERE scene_season = -1 OR scene_episode = -1")
for epResult in sqlResults:
indexerid = int(epResult["showid"])
indexer = int(epResult["indexer"])
season = int(epResult["season"])
episode = int(epResult["episode"])
absolute_number = int(epResult["absolute_number"])
logger.log(
u"Repairing any scene numbering issues for showid: " + str(epResult["showid"]) + u" season: " + str(
epResult["season"]) + u" episode: " + str(epResult["episode"]), logger.DEBUG)
scene_season, scene_episode = sickbeard.scene_numbering.get_scene_numbering(indexerid,
indexer,
season,
episode)
scene_season, scene_episode, scene_absolute_number = sickbeard.scene_numbering.get_scene_numbering(indexerid,
indexer,
season,
episode,
absolute_number)
ql.append(
["UPDATE tv_episodes SET scene_season = ? WHERE indexerid = ?", [scene_season, epResult["indexerid"]]])
ql.append(
["UPDATE tv_episodes SET scene_episode = ? WHERE indexerid = ?", [scene_episode, epResult["indexerid"]]])
ql.append(
["UPDATE tv_episodes SET scene_absolute_number = ? WHERE indexerid = ?",
[scene_absolute_number, epResult["indexerid"]]])
if ql:
myDB.mass_action(ql)

View File

@ -141,7 +141,7 @@ def snatchEpisode(result, endStatus=SNATCHED):
if sickbeard.TORRENT_METHOD == "blackhole":
dlResult = _downloadResult(result)
else:
#Sets per provider seed ratio
# Sets per provider seed ratio
result.ratio = result.provider.seedRatio()
result.content = result.provider.getURL(result.url) if not result.url.startswith('magnet') else None
client = clients.getClientIstance(sickbeard.TORRENT_METHOD)()
@ -298,14 +298,15 @@ def isFirstBestMatch(result):
return False
def filterSearchResults(show, results):
def filterSearchResults(show, season, results):
foundResults = {}
# make a list of all the results for this provider
for curEp in results:
# skip non-tv crap
results[curEp] = filter(
lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, show),
lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, show,
season=season),
results[curEp])
if curEp in foundResults:
@ -354,12 +355,6 @@ def searchForNeededEpisodes(episodes):
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], curEp.show)
# if all results were rejected move on to the next episode
@ -408,7 +403,7 @@ def searchProviders(show, season, episodes, manualSearch=False):
if seasonSearch and provider.search_mode == 'sponly':
search_mode = provider.search_mode
while(True):
while (True):
searchCount += 1
if search_mode == 'sponly':
@ -426,7 +421,20 @@ def searchProviders(show, season, episodes, manualSearch=False):
break
if len(searchResults):
foundResults[provider.name] = filterSearchResults(show, searchResults)
# make a list of all the results for this provider
for curEp in searchResults:
# skip non-tv crap
searchResults[curEp] = filter(
lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name,
show,
season=season),
searchResults[curEp])
if curEp in foundResults:
foundResults[provider.name][curEp] += searchResults[curEp]
else:
foundResults[provider.name][curEp] = searchResults[curEp]
break
elif not provider.search_fallback or searchCount == 2:
break
@ -444,7 +452,6 @@ def searchProviders(show, season, episodes, manualSearch=False):
continue
break
anyQualities, bestQualities = Quality.splitQuality(show.quality)
# pick the best season NZB
@ -511,7 +518,8 @@ def searchProviders(show, season, episodes, manualSearch=False):
individualResults = filter(
lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name,
show),
show,
season=season),
individualResults)
for curResult in individualResults:
@ -563,7 +571,7 @@ def searchProviders(show, season, episodes, manualSearch=False):
if epNum in foundResults[provider.name] and len(foundResults[provider.name][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]):
# if False and multiResult.quality <= pickBestResult(foundResults[epNum]):
# notNeededEps.append(epNum)
#else:
neededEps.append(epNum)

View File

@ -22,7 +22,7 @@ import re
import datetime
import sickbeard
from sickbeard.common import countryList
from sickbeard import common
from sickbeard.helpers import sanitizeSceneName
from sickbeard.scene_exceptions import get_scene_exceptions
from sickbeard import logger
@ -90,7 +90,7 @@ def sceneToNormalShowNames(name):
results.append(re.sub('(\D)(\d{4})$', '\\1(\\2)', cur_name))
# add brackets around the country
country_match_str = '|'.join(countryList.values())
country_match_str = '|'.join(common.countryList.values())
results.append(re.sub('(?i)([. _-])(' + country_match_str + ')$', '\\1(\\2)', cur_name))
results += name_list
@ -98,8 +98,8 @@ def sceneToNormalShowNames(name):
return list(set(results))
def makeSceneShowSearchStrings(show):
showNames = allPossibleShowNames(show)
def makeSceneShowSearchStrings(show, season=-1):
showNames = allPossibleShowNames(show, season=season)
# scenify the names
return map(sanitizeSceneName, showNames)
@ -112,19 +112,43 @@ def makeSceneSeasonSearchString(show, ep_obj, extraSearchType=None):
numseasons = 0
# the search string for air by date shows is just
seasonStrings = [str(ep_obj.airdate).split('-')[0]]
elif show.is_anime:
numseasons = 0
seasonEps = show.getAllEpisodes(ep_obj.season)
# get show qualities
anyQualities, bestQualities = common.Quality.splitQuality(show.quality)
# compile a list of all the episode numbers we need in this 'season'
seasonStrings = []
for episode in seasonEps:
# get quality of the episode
curCompositeStatus = episode.status
curStatus, curQuality = common.Quality.splitCompositeStatus(curCompositeStatus)
if bestQualities:
highestBestQuality = max(bestQualities)
else:
highestBestQuality = 0
# if we need a better one then add it to the list of episodes to fetch
if (curStatus in (
common.DOWNLOADED, common.SNATCHED) and curQuality < highestBestQuality) or curStatus == common.WANTED:
ab_number = episode.scene_absolute_number
if ab_number > 0:
seasonStrings.append("%d" % ab_number)
else:
numseasonsSQlResult = myDB.select(
"SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0",
[show.indexerid])
numseasons = int(numseasonsSQlResult[0][0])
seasonStrings = ["S%02d" % int(ep_obj.scene_season)]
if show.air_by_date or show.sports:
seasonStrings = [str(ep_obj.airdate).split('-')[0]]
else:
seasonStrings = ["S%02d" % int(ep_obj.scene_season)]
showNames = set(makeSceneShowSearchStrings(show))
showNames = set(makeSceneShowSearchStrings(show, ep_obj.scene_season))
toReturn = []
@ -140,9 +164,6 @@ def makeSceneSeasonSearchString(show, ep_obj, extraSearchType=None):
for cur_season in seasonStrings:
toReturn.append(curShow + "." + cur_season)
# episode
toReturn.extend(makeSceneSearchString(show, ep_obj))
return toReturn
@ -152,26 +173,22 @@ def makeSceneSearchString(show, ep_obj):
"SELECT COUNT(DISTINCT season) as numseasons FROM tv_episodes WHERE showid = ? and season != 0",
[show.indexerid])
numseasons = int(numseasonsSQlResult[0][0])
numepisodesSQlResult = myDB.select(
"SELECT COUNT(episode) as numepisodes FROM tv_episodes WHERE showid = ? and season != 0",
[show.indexerid])
numepisodes = int(numepisodesSQlResult[0][0])
# see if we should use dates instead of episodes
if show.air_by_date and ep_obj.airdate != datetime.date.fromordinal(1):
epStrings = [str(ep_obj.airdate)]
elif show.sports:
if (show.air_by_date or show.sports) and ep_obj.airdate != datetime.date.fromordinal(1):
epStrings = [str(ep_obj.airdate)]
elif show.is_anime:
epStrings = ["%i" % int(ep_obj.scene_absolute_number)]
else:
epStrings = ["S%02iE%02i" % (int(ep_obj.scene_season), int(ep_obj.scene_episode)),
"%ix%02i" % (int(ep_obj.scene_season), int(ep_obj.scene_episode))]
# for single-season shows just search for the show name -- if total ep count (exclude s0) is less than 11
# due to the amount of qualities and releases, it is easy to go over the 50 result limit on rss feeds otherwise
if numseasons == 1 and numepisodes < 11:
if numseasons == 1 and not ep_obj.show.is_anime:
epStrings = ['']
showNames = set(makeSceneShowSearchStrings(show))
showNames = set(makeSceneShowSearchStrings(show, ep_obj.scene_season))
toReturn = []
@ -182,20 +199,26 @@ def makeSceneSearchString(show, ep_obj):
return toReturn
def isGoodResult(name, show, log=True):
def isGoodResult(name, show, log=True, season=-1):
"""
Use an automatically-created regex to make sure the result actually is the show it claims to be
"""
all_show_names = allPossibleShowNames(show)
all_show_names = allPossibleShowNames(show, season=season)
showNames = map(sanitizeSceneName, all_show_names) + all_show_names
showNames += map(unidecode, 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+|(?:\d{1,3}.+\d{1,}[a-zA-Z]{2}\W+[a-zA-Z]{3,}\W+\d{4}.+))'
if not show.is_anime:
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+|(?:\d{1,3}.+\d{1,}[a-zA-Z]{2}\W+[a-zA-Z]{3,}\W+\d{4}.+))'
else:
escaped_name = re.sub('\\\\[\\s.-]', '[\W_]+', re.escape(curName))
# FIXME: find a "automatically-created" regex for anime releases # test at http://regexr.com?2uon3
curRegex = '^((\[.*?\])|(\d+[\.-]))*[ _\.]*' + escaped_name + '(([ ._-]+\d+)|([ ._-]+s\d{2})).*'
if log:
logger.log(u"Checking if show " + name + " matches " + curRegex, logger.DEBUG)
@ -209,7 +232,7 @@ def isGoodResult(name, show, log=True):
return False
def allPossibleShowNames(show):
def allPossibleShowNames(show, season=-1):
"""
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.
@ -219,27 +242,33 @@ def allPossibleShowNames(show):
Returns: a list of all the possible show names
"""
showNames = [show.name]
showNames += [name for name in get_scene_exceptions(show.indexerid)]
showNames = get_scene_exceptions(show.indexerid, season=season)
if not showNames: # if we dont have any season specific exceptions fallback to generic exceptions
season = -1
showNames = get_scene_exceptions(show.indexerid, season=season)
if season in [-1, 1]:
showNames.append(show.name)
newShowNames = []
country_list = countryList
country_list.update(dict(zip(countryList.values(), countryList.keys())))
country_list = common.countryList
country_list.update(dict(zip(common.countryList.values(), common.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] + ')'))
if not show.is_anime:
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
showNames += newShowNames
return showNames

View File

@ -132,9 +132,9 @@ class ShowQueue(generic_queue.GenericQueue):
return queueItemObj
def addShow(self, indexer, indexer_id, showDir, default_status=None, quality=None, flatten_folders=None,
subtitles=None, lang="en"):
subtitles=None, lang="en", anime=None):
queueItemObj = QueueItemAdd(indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang,
subtitles)
subtitles, anime)
self.add_item(queueItemObj)
@ -189,7 +189,7 @@ class ShowQueueItem(generic_queue.QueueItem):
class QueueItemAdd(ShowQueueItem):
def __init__(self, indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles):
def __init__(self, indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime):
self.indexer = indexer
self.indexer_id = indexer_id
@ -199,6 +199,7 @@ class QueueItemAdd(ShowQueueItem):
self.flatten_folders = flatten_folders
self.lang = lang
self.subtitles = subtitles
self.anime = anime
self.show = None
@ -283,6 +284,7 @@ class QueueItemAdd(ShowQueueItem):
self.show.subtitles = self.subtitles if self.subtitles != None else sickbeard.SUBTITLES_DEFAULT
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.anime = self.anime if self.anime != None else sickbeard.ANIME_DEFAULT
self.show.paused = False
# be smartish about this
@ -292,7 +294,8 @@ class QueueItemAdd(ShowQueueItem):
self.show.air_by_date = 0
if self.show.classification and "sports" in self.show.classification.lower():
self.show.sports = 1
if self.show.genre and "animation" in self.show.genre.lower():
self.show.anime = 1
except sickbeard.indexer_exception, e:
logger.log(

View File

@ -79,6 +79,7 @@ class TVShow(object):
self.archive_firstmatch = 0
self.lang = lang
self.last_update_indexer = 1
self.anime = 0
self.rls_ignore_words = ""
self.rls_require_words = ""
@ -94,6 +95,20 @@ class TVShow(object):
self.loadFromDB()
def _is_anime(self):
if(self.anime > 0):
return True
else:
return False
is_anime = property(_is_anime)
def _is_sports(self):
if(self.sports > 0):
return True
else:
return False
is_sports = property(_is_sports)
def _getLocation(self):
# no dir check needed if missing show dirs are created during post-processing
if sickbeard.CREATE_MISSING_SHOW_DIRS:
@ -166,13 +181,30 @@ class TVShow(object):
return ep_list
def getEpisode(self, season, episode, file=None, noCreate=False):
def getEpisode(self, season, episode, file=None, noCreate=False, absolute_number=None):
if not season in self.episodes:
self.episodes[season] = {}
ep = None
# if we get an anime get the real season and episode
if self.anime and absolute_number != None and season == None and episode == None:
myDB = db.DBConnection()
sql = "SELECT * FROM tv_episodes WHERE showid = ? and absolute_number = ? and season != 0"
sqlResults = myDB.select(sql, [self.indexerid, absolute_number])
if len(sqlResults) == 1:
episode = int(sqlResults[0]["episode"])
season = int(sqlResults[0]["season"])
logger.log("Found episode by absolute_number:"+str(absolute_number)+" which is "+str(season)+"x"+str(episode), logger.DEBUG)
elif len(sqlResults) > 1:
logger.log("Multiple entries for absolute number: "+str(absolute_number)+" in show: "+self.name+" found ", logger.ERROR)
return None
else:
logger.log("No entries for absolute number: "+str(absolute_number)+" in show: "+self.name+" found.", logger.DEBUG)
return None
if not episode in self.episodes[season] or self.episodes[season][episode] == None:
if noCreate:
return None
@ -190,6 +222,7 @@ class TVShow(object):
epObj = self.episodes[season][episode]
epObj.convertToSceneNumbering()
return epObj
def should_update(self, update_date=datetime.date.today()):
@ -515,7 +548,7 @@ class TVShow(object):
logger.log(str(self.indexerid) + u": Creating episode object from " + file, logger.DEBUG)
try:
myParser = NameParser()
myParser = NameParser(show=self, useIndexers=True)
parse_result = myParser.parse(file)
except InvalidNameException:
logger.log(u"Unable to parse the filename " + file + " into a valid episode", logger.ERROR)
@ -601,7 +634,7 @@ class TVShow(object):
# 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)
newQuality = Quality.nameQuality(file, self.is_anime)
logger.log(u"Since this file has been renamed, I checked " + file + " and found quality " +
Quality.qualityStrings[newQuality], logger.DEBUG)
if newQuality != Quality.UNKNOWN:
@ -613,7 +646,7 @@ class TVShow(object):
ARCHIVED, IGNORED]:
oldStatus, oldQuality = Quality.splitCompositeStatus(curEp.status)
newQuality = Quality.nameQuality(file)
newQuality = Quality.nameQuality(file, self.is_anime)
if newQuality == Quality.UNKNOWN:
newQuality = Quality.assumeQuality(file)
@ -720,6 +753,10 @@ class TVShow(object):
if not self.lang:
self.lang = sqlResults[0]["lang"]
self.anime = sqlResults[0]["anime"]
if self.anime == None:
self.anime = 0
self.last_update_indexer = sqlResults[0]["last_update_indexer"]
self.rls_ignore_words = sqlResults[0]["rls_ignore_words"]
@ -1040,12 +1077,14 @@ class TVShow(object):
"archive_firstmatch": self.archive_firstmatch,
"startyear": self.startyear,
"lang": self.lang,
"anime": self.anime,
"imdb_id": self.imdbid,
"last_update_indexer": self.last_update_indexer,
"rls_ignore_words": self.rls_ignore_words,
"rls_require_words": self.rls_require_words
}
myDB.upsert("tv_shows", newValueDict, controlValueDict)
helpers.update_anime_support()
if self.imdbid:
controlValueDict = {"indexer_id": self.indexerid}
@ -1071,6 +1110,7 @@ class TVShow(object):
toReturn += "classification: " + self.classification + "\n"
toReturn += "runtime: " + str(self.runtime) + "\n"
toReturn += "quality: " + str(self.quality) + "\n"
toReturn += "anime: " + str(self.is_anime) + "\n"
return toReturn
@ -1183,8 +1223,10 @@ class TVEpisode(object):
self._name = ""
self._season = season
self._episode = episode
self._absolute_number = 0
self._scene_season = season
self._scene_episode = episode
self._scene_absolute_number = 0
self._description = ""
self._subtitles = list()
self._subtitles_searchcount = 0
@ -1218,8 +1260,10 @@ class TVEpisode(object):
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"))
absolute_number = property(lambda self: self._absolute_number, dirty_setter("_absolute_number"))
scene_season = property(lambda self: self._scene_season, dirty_setter("_scene_season"))
scene_episode = property(lambda self: self._scene_episode, dirty_setter("_scene_episode"))
scene_absolute_number = property(lambda self: self._scene_absolute_number, dirty_setter("_scene_absolute_number"))
description = property(lambda self: self._description, dirty_setter("_description"))
subtitles = property(lambda self: self._subtitles, dirty_setter("_subtitles"))
subtitles_searchcount = property(lambda self: self._subtitles_searchcount, dirty_setter("_subtitles_searchcount"))
@ -1396,6 +1440,7 @@ class TVEpisode(object):
self.season = season
self.episode = episode
self.absolute_number = sqlResults[0]["absolute_number"]
self.description = sqlResults[0]["description"]
if not self.description:
self.description = ""
@ -1488,6 +1533,14 @@ class TVEpisode(object):
self.deleteEpisode()
return False
if myEp["absolute_number"] == None or myEp["absolute_number"] == "":
logger.log(u"This episode ("+self.show.name+" - "+str(season)+"x"+str(episode)+") has no absolute number on " + sickbeard.indexerApi(
self.indexer).name
, logger.DEBUG)
else:
logger.log(str(self.show.indexerid) + ": The absolute_number for " + str(season) + "x" + str(episode)+" is : "+myEp["absolute_number"], logger.DEBUG)
self.absolute_number = int(myEp["absolute_number"])
self.name = getattr(myEp, 'episodename', "")
self.season = season
self.episode = episode
@ -1724,12 +1777,12 @@ class TVEpisode(object):
# use a custom update/insert method to get the data into the DB
return [
"INSERT OR REPLACE INTO tv_episodes (episode_id, indexerid, indexer, name, description, subtitles, subtitles_searchcount, subtitles_lastsearch, airdate, hasnfo, hastbn, status, location, file_size, release_name, is_proper, showid, season, episode) VALUES "
"((SELECT episode_id FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
"INSERT OR REPLACE INTO tv_episodes (episode_id, indexerid, indexer, name, description, subtitles, subtitles_searchcount, subtitles_lastsearch, airdate, hasnfo, hastbn, status, location, file_size, release_name, is_proper, showid, season, episode, absolute_number) VALUES "
"((SELECT episode_id FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
[self.show.indexerid, self.season, self.episode, self.indexerid, self.indexer, self.name, self.description,
",".join([sub for sub in self.subtitles]), self.subtitles_searchcount, self.subtitles_lastsearch,
self.airdate.toordinal(), self.hasnfo, self.hastbn, self.status, self.location, self.file_size,
self.release_name, self.is_proper, self.show.indexerid, self.season, self.episode]]
self.release_name, self.is_proper, self.show.indexerid, self.season, self.episode, self.absolute_number]]
def saveToDB(self, forceSave=False):
"""
@ -1763,7 +1816,9 @@ class TVEpisode(object):
"location": self.location,
"file_size": self.file_size,
"release_name": self.release_name,
"is_proper": self.is_proper}
"is_proper": self.is_proper,
"absolute_number": self.absolute_number
}
controlValueDict = {"showid": self.show.indexerid,
"season": self.season,
"episode": self.episode}
@ -1784,6 +1839,7 @@ class TVEpisode(object):
Returns: A string representing the episode's name and season/ep numbers
"""
return self._format_pattern('%SN - %Sx%0E - %EN')
def prettySceneName(self):
@ -1898,6 +1954,7 @@ class TVEpisode(object):
'%0XS': '%02d' % self.scene_season,
'%XE': str(self.scene_episode),
'%0XE': '%02d' % self.scene_episode,
'%AN': '%03d' % self.absolute_number,
'%RN': release_name(self.release_name),
'%RG': release_group(self.release_name),
'%AD': str(self.airdate).replace('-', ' '),
@ -2183,13 +2240,15 @@ class TVEpisode(object):
relEp.saveToDB()
def convertToSceneNumbering(self):
(self.scene_season, self.scene_episode) = sickbeard.scene_numbering.get_scene_numbering(self.show.indexerid,
(self.scene_season, self.scene_episode, self.scene_absolute_number) = sickbeard.scene_numbering.get_scene_numbering(self.show.indexerid,
self.show.indexer,
self.season,
self.episode)
self.episode,
self.absolute_number)
def convertToIndexerNumbering(self):
(self.season, self.episode) = sickbeard.scene_numbering.get_indexer_numbering(self.show.indexerid,
(self.season, self.episode, self.absolute_number) = sickbeard.scene_numbering.get_indexer_numbering(self.show.indexerid,
self.show.indexer,
self.scene_season,
self.scene_episode)
self.scene_episode,
self.scene_absolute_number)

View File

@ -145,7 +145,7 @@ class TVCache():
parsed[2] = re.sub("/{2,}", "/", parsed[2]) # replace two or more / with one
if post_data:
url = url + 'api?' + urllib.urlencode(post_data)
url += urllib.urlencode(post_data)
f = fc.fetch(url)
@ -333,7 +333,7 @@ class TVCache():
# get quality of release
if quality is None:
quality = Quality.sceneQuality(name)
quality = Quality.sceneQuality(name, parse_result.is_anime)
if not isinstance(name, unicode):
name = unicode(name, 'utf-8')

View File

@ -59,7 +59,6 @@ from sickbeard.webapi import Api
from sickbeard.scene_exceptions import get_scene_exceptions
from sickbeard.scene_numbering import get_scene_numbering, set_scene_numbering, get_scene_numbering_for_show, \
get_xem_numbering_for_show
from sickbeard.providers.generic import TorrentProvider
from lib.dateutil import tz
from lib.unrar2 import RarFile, RarInfo
@ -77,7 +76,7 @@ except ImportError:
import xml.etree.ElementTree as etree
from sickbeard import browser
from lib import adba
def _handle_reverse_proxy():
if sickbeard.HANDLE_REVERSE_PROXY:
@ -565,6 +564,9 @@ class Manage:
paused_all_same = True
last_paused = None
anime_all_same = True
last_anime = None
quality_all_same = True
last_quality = None
@ -587,6 +589,13 @@ class Manage:
else:
last_paused = curShow.paused
if anime_all_same:
# if we had a value already and this value is different then they're not all the same
if last_anime not in (curShow.is_anime, None):
anime_all_same = False
else:
last_anime = curShow.is_anime
if flatten_folders_all_same:
if last_flatten_folders not in (None, curShow.flatten_folders):
flatten_folders_all_same = False
@ -607,6 +616,7 @@ class Manage:
t.showList = toEdit
t.paused_value = last_paused if paused_all_same else None
t.anime_value = last_anime if anime_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.subtitles_value = last_subtitles if subtitles_all_same else None
@ -615,7 +625,7 @@ class Manage:
return _munge(t)
@cherrypy.expose
def massEditSubmit(self, paused=None, flatten_folders=None, quality_preset=False, subtitles=None,
def massEditSubmit(self, paused=None, anime=None, flatten_folders=None, quality_preset=False, subtitles=None,
anyQualities=[], bestQualities=[], toEdit=None, *args, **kwargs):
dir_map = {}
@ -649,6 +659,12 @@ class Manage:
new_paused = True if paused == 'enable' else False
new_paused = 'on' if new_paused else 'off'
if anime == 'keep':
new_anime = showObj.is_anime
else:
new_anime = True if anime == 'enable' else False
new_anime = 'on' if new_anime else 'off'
if flatten_folders == 'keep':
new_flatten_folders = showObj.flatten_folders
else:
@ -668,7 +684,7 @@ class Manage:
exceptions_list = []
curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, exceptions_list,
new_flatten_folders, new_paused, subtitles=new_subtitles, directCall=True)
new_flatten_folders, new_paused, subtitles=new_subtitles, anime=new_anime, directCall=True)
if curErrors:
logger.log(u"Errors: " + str(curErrors), logger.ERROR)
@ -942,6 +958,7 @@ ConfigMenu = [
{'title': 'Subtitles Settings', 'path': 'config/subtitles/'},
{'title': 'Post Processing', 'path': 'config/postProcessing/'},
{'title': 'Notifications', 'path': 'config/notifications/'},
{'title': 'Anime', 'path': 'config/anime/'},
]
@ -958,7 +975,7 @@ class ConfigGeneral:
sickbeard.ROOT_DIRS = rootDirString
@cherrypy.expose
def saveAddShowDefaults(self, defaultStatus, anyQualities, bestQualities, defaultFlattenFolders, subtitles=False):
def saveAddShowDefaults(self, defaultStatus, anyQualities, bestQualities, defaultFlattenFolders, subtitles=False, anime=False):
if anyQualities:
anyQualities = anyQualities.split(',')
@ -978,6 +995,8 @@ class ConfigGeneral:
sickbeard.FLATTEN_FOLDERS_DEFAULT = config.checkbox_to_value(defaultFlattenFolders)
sickbeard.SUBTITLES_DEFAULT = config.checkbox_to_value(subtitles)
sickbeard.ANIME_DEFAULT = int(anime)
sickbeard.save_config()
@cherrypy.expose
@ -1185,7 +1204,7 @@ class ConfigPostProcessing:
wdtv_data=None, tivo_data=None, mede8er_data=None,
keep_processed_dir=None, process_method=None, process_automatically=None,
rename_episodes=None, airdate_episodes=None, unpack=None,
move_associated_files=None, tv_download_dir=None, naming_custom_abd=None,
move_associated_files=None, tv_download_dir=None, naming_custom_abd=None, naming_anime=None,
naming_abd_pattern=None, naming_strip_year=None, use_failed_downloads=None,
delete_failed=None, extra_scripts=None, skip_removed_files=None,
naming_custom_sports=None, naming_sports_pattern=None, autopostprocesser_frequency=None):
@ -1221,6 +1240,7 @@ class ConfigPostProcessing:
sickbeard.NAMING_CUSTOM_ABD = config.checkbox_to_value(naming_custom_abd)
sickbeard.NAMING_CUSTOM_SPORTS = config.checkbox_to_value(naming_custom_sports)
sickbeard.NAMING_STRIP_YEAR = config.checkbox_to_value(naming_strip_year)
sickbeard.NAMING_ANIME = config.checkbox_to_value(naming_anime)
sickbeard.USE_FAILED_DOWNLOADS = config.checkbox_to_value(use_failed_downloads)
sickbeard.DELETE_FAILED = config.checkbox_to_value(delete_failed)
sickbeard.SKIP_REMOVED_FILES = config.checkbox_to_value(skip_removed_files)
@ -1273,12 +1293,12 @@ class ConfigPostProcessing:
redirect("/config/postProcessing/")
@cherrypy.expose
def testNaming(self, pattern=None, multi=None, abd=False, sports=False):
def testNaming(self, pattern=None, multi=None, abd=False, sports=False, anime=None):
if multi is not None:
multi = int(multi)
result = naming.test_name(pattern, multi, abd, sports)
result = naming.test_name(pattern, multi, abd, sports, anime)
result = ek.ek(os.path.join, result['dir'], result['name'])
@ -1966,6 +1986,52 @@ class ConfigSubtitles:
redirect("/config/subtitles/")
class ConfigAnime:
@cherrypy.expose
def index(self):
t = PageTemplate(file="config_anime.tmpl")
t.submenu = ConfigMenu
return _munge(t)
@cherrypy.expose
def saveAnime(self, use_anidb=None, anidb_username=None, anidb_password=None, anidb_use_mylist=None, split_home=None):
results = []
if use_anidb == "on":
use_anidb = 1
else:
use_anidb = 0
if anidb_use_mylist == "on":
anidb_use_mylist = 1
else:
anidb_use_mylist = 0
if split_home == "on":
split_home = 1
else:
split_home = 0
sickbeard.USE_ANIDB = use_anidb
sickbeard.ANIDB_USERNAME = anidb_username
sickbeard.ANIDB_PASSWORD = anidb_password
sickbeard.ANIDB_USE_MYLIST = anidb_use_mylist
sickbeard.ANIME_SPLIT_HOME = split_home
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/anime/")
class Config:
@cherrypy.expose
@ -1987,6 +2053,7 @@ class Config:
subtitles = ConfigSubtitles()
anime = ConfigAnime()
def haveXBMC():
return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY
@ -2250,7 +2317,7 @@ class NewHomeAddShows:
@cherrypy.expose
def addNewShow(self, whichSeries=None, indexerLang="en", rootDir=None, defaultStatus=None,
anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None,
fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None):
fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=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.
@ -2295,7 +2362,7 @@ class NewHomeAddShows:
indexer_id = int(series_pieces[3])
show_name = series_pieces[4]
else:
indexer = 1
indexer = int(providedIndexer)
indexer_id = int(whichSeries)
show_name = os.path.basename(os.path.normpath(fullShowPath))
@ -2324,9 +2391,11 @@ class NewHomeAddShows:
helpers.chmodAsParent(show_dir)
# prepare the inputs for passing along
anime = config.checkbox_to_value(anime)
flatten_folders = config.checkbox_to_value(flatten_folders)
subtitles = config.checkbox_to_value(subtitles)
if not anyQualities:
anyQualities = []
if not bestQualities:
@ -2339,7 +2408,7 @@ class NewHomeAddShows:
# add the show
sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, int(defaultStatus), newQuality,
flatten_folders, subtitles, indexerLang) # @UndefinedVariable
flatten_folders, subtitles, indexerLang, anime) # @UndefinedVariable
ui.notifications.message('Show added', 'Adding the specified show into ' + show_dir)
return finishAddShow()
@ -2522,6 +2591,20 @@ class Home:
def index(self):
t = PageTemplate(file="home.tmpl")
if sickbeard.ANIME_SPLIT_HOME:
shows = []
anime = []
for show in sickbeard.showList:
if show.is_anime:
anime.append(show)
else:
shows.append(show)
t.showlists = [["Shows",shows],
["Anime",anime]]
else:
t.showlists = [["Shows",sickbeard.showList]]
t.submenu = HomeMenu()
return _munge(t)
@ -2946,7 +3029,18 @@ class Home:
x = x[4:]
return x
t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))
if sickbeard.ANIME_SPLIT_HOME:
shows = []
anime = []
for show in sickbeard.showList:
if show.is_anime:
anime.append(show)
else:
shows.append(show)
t.sortedShowLists = [["Shows",sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))],
["Anime",sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]]
else:
t.sortedShowLists = [["Shows",sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]]
t.epCounts = epCounts
t.epCats = epCats
@ -2966,11 +3060,24 @@ class Home:
(int(show), int(season), int(episode))).fetchone()
return result['description'] if result else 'Episode not found.'
@cherrypy.expose
def sceneExceptions(self, show):
exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(show)
if not exceptionsList:
return "No scene exceptions"
out = []
for season, names in iter(sorted(exceptionsList.iteritems())):
if season == -1:
season = "*"
out.append("S" + str(season) + ": " + ", ".join(names))
return "<br/>".join(out)
@cherrypy.expose
def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], exceptions_list=[],
flatten_folders=None, paused=None, directCall=False, air_by_date=None, sports=None, dvdorder=None,
indexerLang=None, subtitles=None, archive_firstmatch=None, rls_ignore_words=None,
rls_require_words=None):
rls_require_words=None, anime=None):
if show is None:
errString = "Invalid show ID: " + str(show)
@ -2993,6 +3100,13 @@ class Home:
if not location and not anyQualities and not bestQualities and not flatten_folders:
t = PageTemplate(file="editShow.tmpl")
t.submenu = HomeMenu()
if showObj.is_anime:
t.groups = []
if helpers.set_up_anidb_connection():
anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=showObj.name)
t.groups = anime.get_groups()
with showObj.lock:
t.show = showObj
@ -3008,6 +3122,7 @@ class Home:
paused = config.checkbox_to_value(paused)
air_by_date = config.checkbox_to_value(air_by_date)
sports = config.checkbox_to_value(sports)
anime = config.checkbox_to_value(anime)
subtitles = config.checkbox_to_value(subtitles)
indexer_lang = indexerLang
@ -3055,6 +3170,7 @@ class Home:
if not directCall:
showObj.air_by_date = air_by_date
showObj.sports = sports
showObj.anime = anime
showObj.subtitles = subtitles
showObj.lang = indexer_lang
showObj.dvdorder = dvdorder

View File

@ -81,7 +81,7 @@ sickbeard.NAMING_MULTI_EP = 1
sickbeard.PROVIDER_ORDER = ["sick_beard_index"]
sickbeard.newznabProviderList = providers.getNewznabProviderList("'SickRage Index|http://lolo.sickbeard.com/|0|5030,5040,5060|0|eponly|0!!!NZBs.org|https://nzbs.org/||5030,5040,5060,5070,5090|0|eponly|0!!!Usenet-Crawler|https://www.usenet-crawler.com/||5030,5040,5060|0|eponly|0'")
sickbeard.newznabProviderList = providers.getNewznabProviderList("'Sick Beard Index|http://lolo.sickbeard.com/|0|5030,5040,5060|0|eponly|0!!!NZBs.org|https://nzbs.org/||5030,5040,5060,5070,5090|0|eponly|0!!!Usenet-Crawler|https://www.usenet-crawler.com/||5030,5040,5060|0|eponly|0'")
sickbeard.providerList = providers.makeProviderList()
sickbeard.PROG_DIR = os.path.abspath('..')