#!/usr/bin/env python # coding=utf-8 # # 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 logging import os import sys import threading from datetime import timedelta from time import sleep, time from configparser import ConfigParser from .aniDBlink import AniDBLink from .aniDBcommands import * from .aniDBerrors import * from .aniDBAbstracter import Anime, Episode version = 100 logger = logging.getLogger('adba') logger.addHandler(logging.NullHandler()) class Connection(threading.Thread): def __init__(self, clientname='adba', server='api.anidb.info', port=9000, myport=9876, user=None, password=None, session=None, keepAlive=False, commandDelay=4.1): super(Connection, self).__init__() self.link = AniDBLink(server, port, myport, delay=commandDelay) self.link.session = session self.clientname = clientname self.clientver = version # from original lib self.mode = 1 # mode: 0=queue,1=unlock,2=callback # Filename to maintain session info always in script directory self.SessionFile = os.path.normpath(sys.path[0] + os.sep + "Session.cfg") # to lock other threads out self.lock = threading.RLock() # last Command Time set to now even though no commands are sent yet self.LastCommandTime = time() # thread keep alive stuff self.keepAlive = keepAlive self.daemon = True self.lastKeepAliveCheck = 0 self.lastAuth = 0 self._username = password self._password = user self._iamALIVE = False self.counter = 0 self.counterAge = 0 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': logger.debug("seems like the last command got a not authed error back trying to reconnect now") if self._reAuthenticate(): response.req.resp = None 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 happened 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) logger.debug("handling({counter}-{delay}) command {command}".format(counter=self.counter, delay=self.link.delay, command=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 not in (None, '')) if not authed and (reAuthenticate or self.keepAlive): self._reAuthenticate() authed = (self.link.session not in (None, '')) self.lock.release() return authed def _reAuthenticate(self): if self._username and self._password: logger.info("auto re authenticating !") resp = self.auth(self._username, self._password) if resp and resp.rescode != '500': return True else: return False def _keep_alive(self): self.lastKeepAliveCheck = time() logger.info("auto check !") # check every 30 minutes if the session is still valid # if not reauthenticate if self.lastAuth and time() - self.lastAuth > 1800: logger.info("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: logger.info("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) """ if self.keepAlive: self._username = username self._password = password if False is self.is_alive(): logger.debug("You wanted to keep this thing alive!") if False is self._iamALIVE: logger.info("Starting thread now...") self.start() self._iamALIVE = True else: logger.info("not starting thread seems like it is already running. this must be a _reAuthenticate") config = ConfigParser() config.read(self.SessionFile) needauth = False try: if config.getboolean('DEFAULT', 'loggedin'): self.lastCommandTime = config.getfloat('DEFAULT', 'lastcommandtime') timeelapsed = time() - self.lastCommandTime timeoutduration = timedelta(minutes=30).seconds if timeelapsed < timeoutduration: # we are logged in and within timeout so set up session key and assume valid self.link.session = config.get('DEFAULT', 'sessionkey') if not self.link.session: needauth = True else: needauth = True else: needauth = True except: needauth = True if needauth: self.lastAuth = time() logger.debug('No valid session, so authenticating') try: self.handle(AuthCommand(username, password, 3, self.clientname, self.clientver, nat, 1, 'utf8', mtu), callback) except Exception as error: logger.debug('Auth command with exception %r' % error) # we force a config file with logged out to ensure a known state if an exception occurs, forcing us to log in again config['DEFAULT'] = {'loggedin': 'yes', 'sessionkey': str(self.link.session or ''), 'exception': str(error), 'lastcommandtime': repr(time())} with open(self.SessionFile, 'w') as configfile: config.write(configfile) return error logger.debug('Successfully authenticated and recording session details') config['DEFAULT'] = {'loggedin': 'yes', 'sessionkey': str(self.link.session or ''), 'lastcommandtime': repr(time())} with open(self.SessionFile, 'w') as configfile: config.write(configfile) return def logout(self, cutConnection=True, callback=None): """ Log out from AniDB UDP API """ config = ConfigParser() config.read(self.SessionFile) if config['DEFAULT']['loggedin'] == 'yes': self.link.session = config.get('DEFAULT', 'sessionkey') result = self.handle(LogoutCommand(), callback) if cutConnection: self.cut() config['DEFAULT']['loggedin'] = 'no' with open(self.SessionFile, 'w') as configfile: config.write(configfile) logger.debug('Logging out') return result logger.debug('Not logging out') return def stayloggedin(self): """ handles timeout constraints of the link before exiting """ config = ConfigParser() config.read(self.SessionFile) config['DEFAULT']['lastcommandtime'] = repr(time()) with open(self.SessionFile, 'w') as configfile: config.write(configfile) self.link._do_delay() logger.debug('Staying logged in') return 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 NotImplementedError("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") 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)