You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

anidb.py 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. from datetime import datetime, timedelta
  2. import os
  3. import random
  4. import socket
  5. import string
  6. import sys
  7. import time
  8. from libkiara import AbandonShip
  9. CLIENT_NAME = "kiara"
  10. CLIENT_VERSION = "4"
  11. CLIENT_ANIDB_PROTOVER = "3"
  12. LOGIN_ACCEPTED = '200'
  13. LOGIN_ACCEPTED_OUTDATED_CLIENT = '201'
  14. LOGGED_OUT = '203'
  15. MYLIST_ENTRY_ADDED = '210'
  16. FILE = '220'
  17. PONG = '300'
  18. FILE_ALREADY_IN_MYLIST = '310'
  19. MYLIST_ENTRY_EDITED = '311'
  20. NO_SUCH_FILE = '320'
  21. NOT_LOGGED_IN = '403'
  22. LOGIN_FAILED = '500'
  23. LOGIN_FIRST = '501'
  24. ACCESS_DENIED = '502'
  25. CLIENT_VERSION_OUTDATED = '503'
  26. CLIENT_BANNED = '504'
  27. ILLEGAL_INPUT = '505'
  28. INVALID_SESSION = '506'
  29. BANNED = '555'
  30. UNKNOWN_COMMAND = '598'
  31. INTERNAL_SERVER_ERROR = '600'
  32. OUT_OF_SERVICE = '601'
  33. SERVER_BUSY = '602'
  34. DIE_MESSAGES = [
  35. BANNED, ILLEGAL_INPUT, UNKNOWN_COMMAND,
  36. INTERNAL_SERVER_ERROR, ACCESS_DENIED
  37. ]
  38. LATER_MESSAGES = [OUT_OF_SERVICE, SERVER_BUSY]
  39. REAUTH_MESSAGES = [LOGIN_FIRST, INVALID_SESSION]
  40. # This will get overridden from kiarad.py
  41. config = None
  42. session_key = None
  43. sock = None
  44. # anidb specifies a hard limit that no more than one message every 2 seconds
  45. # may me send, and a soft one at one message every 4 seconds over an 'extended
  46. # period'. 3 seconds is... faster than 4...
  47. message_interval = timedelta(seconds=3)
  48. next_message = datetime.now()
  49. # Wrap outputting
  50. OUTPUT = None
  51. output_queue = list()
  52. def output(*args):
  53. global OUTPUT
  54. try:
  55. if OUTPUT:
  56. OUTPUT(list(args))
  57. except TypeError: # OUTPUT is not a function.
  58. OUTPUT = None
  59. output_queue.append(args)
  60. def set_output(o):
  61. global OUTPUT
  62. while output_queue:
  63. o(output_queue.pop(0))
  64. OUTPUT = o
  65. def tag_gen(length=5):
  66. """ Makes random strings for use as tags, so messages from anidb will not
  67. get mixed up. """
  68. return "".join([
  69. random.choice(string.ascii_letters)
  70. for _ in range(length)])
  71. def _comm(command, **kwargs):
  72. global next_message, session_key
  73. assert sock != None
  74. wait = (next_message - datetime.now()).total_seconds()
  75. if wait > 0:
  76. time.sleep(wait)
  77. next_message = datetime.now() + message_interval
  78. # Add a tag
  79. tag = tag_gen()
  80. kwargs['tag'] = tag
  81. # And the session key, if we have one
  82. if session_key:
  83. kwargs['s'] = session_key
  84. # Send shit.
  85. shit = (command + " " + "&".join(
  86. map(lambda k: "%s=%s" % (k, kwargs[k]), kwargs)))
  87. output('debug', '_',
  88. '--> %s' % (shit if command is not 'AUTH' else 'AUTH (hidden)'))
  89. sock.send(shit.encode('ascii'))
  90. # Receive shit
  91. while True:
  92. try:
  93. reply = sock.recv(1400).decode().strip()
  94. except socket.timeout:
  95. # Wait...
  96. output('status', 'socket_timeout')
  97. time.sleep(10)
  98. try:
  99. reply = sock.recv(1400).decode().strip()
  100. except socket.timeout:
  101. # Retry it only once. If this fails, anidb is either broken, or
  102. # blocking us
  103. output('error', 'socket_timeout_again')
  104. raise AbandonShip
  105. output('debug', '_', '<-- %s' % reply)
  106. if reply[0:3] == "555" or reply[6:9] == '555':
  107. output('error', 'banned', reply)
  108. raise AbandonShip
  109. return_tag, code, data = reply.split(' ', 2)
  110. if return_tag == tag:
  111. break
  112. else:
  113. output('debug', 'wrong_tag')
  114. # If this was a transmission error, or an anidb error, we will hit
  115. # a timeout and die...
  116. if code in DIE_MESSAGES:
  117. output('error', 'oh_no', code, data)
  118. raise AbandonShip
  119. if code in LATER_MESSAGES:
  120. output('error', 'anidb_busy')
  121. raise AbandonShip
  122. if code in REAUTH_MESSAGES:
  123. output('status', 'login_again', code, data)
  124. _connect(force=True)
  125. return _comm(command, **kwargs)
  126. return code, data
  127. def ping(redirect):
  128. set_output(redirect.reply)
  129. _connect(needs_auth=False)
  130. code, reply = _comm('PING')
  131. if code == PONG:
  132. return True
  133. output('error', 'unexpected_reply', code, reply)
  134. return False
  135. def _connect(force=False, needs_auth=True):
  136. global session_key, sock
  137. if not sock:
  138. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  139. sock.connect((config['host'], int(config['port'])))
  140. sock.settimeout(10)
  141. # If we have a session key, we assume that we are connected.
  142. if (not session_key and needs_auth) or force:
  143. output('status', 'logging_in')
  144. code, key = _comm(
  145. 'AUTH',
  146. user=config['user'],
  147. protover=CLIENT_ANIDB_PROTOVER,
  148. client=CLIENT_NAME,
  149. clientver=CLIENT_VERSION,
  150. **{'pass': config['pass']} # We cannot use pass as a name :(
  151. )
  152. if code == LOGIN_ACCEPTED_OUTDATED_CLIENT:
  153. output('status', 'login_accepted_outdated_client')
  154. elif code == LOGIN_ACCEPTED:
  155. pass
  156. elif code in CLIENT_VERSION_OUTDATED:
  157. output('error', 'kiara_outdated')
  158. raise AbandonShip
  159. elif code in CLIENT_BANNED:
  160. output('error', 'kiara_banned')
  161. sys.exit()
  162. else:
  163. output('error', 'login_unexpected_return', code, key)
  164. raise AbandonShip
  165. session_key = key.split()[0]
  166. output('status', 'login_successful')
  167. output('debug', 'login_session_key', session_key)
  168. def _type_map(ext):
  169. if ext in ['mpg', 'mpeg', 'avi', 'mkv', 'ogm', 'mp4', 'wmv']:
  170. return 'vid'
  171. if ext in ['ssa', 'sub', 'ass']:
  172. return 'sub'
  173. if ext in ['flac', 'mp3']:
  174. return 'snd'
  175. output('error', 'unknown_file_extension', str(ext))
  176. return None
  177. def load_info(thing, redirect):
  178. set_output(redirect.reply)
  179. _connect()
  180. lookup = {
  181. 'fmask': '48080100a0',
  182. 'amask': '90808040',
  183. }
  184. if thing.fid:
  185. lookup['fid'] = thing.fid
  186. else:
  187. lookup['size'] = thing.size
  188. lookup['ed2k'] = thing.hash
  189. code, reply = _comm('FILE', **lookup)
  190. if code == NO_SUCH_FILE:
  191. output('error', 'anidb_file_unknown')
  192. elif code == FILE:
  193. parts = reply.split('\n')[1].split('|')
  194. parts.reverse()
  195. thing.fid = int(parts.pop())
  196. thing.aid = int(parts.pop())
  197. thing.mylist_id = int(parts.pop())
  198. thing.crc32 = parts.pop()
  199. thing.file_type = _type_map(parts.pop())
  200. output('debug', 'file_type', thing.file_type)
  201. thing.added = parts.pop() == '1'
  202. thing.watched = parts.pop() == '1'
  203. thing.anime_total_eps = int(parts.pop())
  204. thing.anime_type = parts.pop()
  205. thing.anime_name = parts.pop()
  206. thing.ep_no = parts.pop()
  207. thing.group_name = parts.pop()
  208. thing.updated = datetime.now()
  209. thing.dirty = True
  210. def add(thing, redirect):
  211. set_output(redirect.reply)
  212. _connect()
  213. code, reply = _comm('MYLISTADD',
  214. fid=str(thing.fid),
  215. state='1')
  216. if code == MYLIST_ENTRY_ADDED:
  217. thing.mylist_id = reply.split('\n')[1]
  218. thing.added = True
  219. thing.dirty = True
  220. output('success', 'file_added')
  221. elif code == FILE_ALREADY_IN_MYLIST:
  222. thing.mylist_id = reply.split('\n')[1].split('|')[0]
  223. thing.added = True
  224. thing.dirty = True
  225. output('success', 'file_added')
  226. else:
  227. output('error', 'unexpected_reply', code, reply)
  228. def watch(thing, redirect):
  229. set_output(redirect.reply)
  230. _connect()
  231. code, reply = _comm('MYLISTADD',
  232. lid=str(thing.mylist_id),
  233. edit='1', state='1', viewed='1')
  234. if code == MYLIST_ENTRY_EDITED:
  235. thing.watched = True
  236. thing.dirty = True
  237. output('success', 'file_marked_watched')
  238. else:
  239. output('error', 'unexpected_reply', code, reply)