PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/share/contrib/tmradio.net/hotline/hotline.py

https://code.google.com/p/ardj/
Python | 374 lines | 339 code | 24 blank | 11 comment | 9 complexity | d62e2d4a4767f5d33134dcad96a45f66 MD5 | raw file
  1. #!/usr/bin/env python
  2. # vim: set fileencoding=utf-8 tw=0:
  3. import email
  4. import email.header
  5. import email.parser
  6. import logging
  7. import logging.handlers
  8. import os
  9. import re
  10. import rfc822
  11. import subprocess
  12. import sys
  13. import tempfile
  14. import time
  15. import traceback
  16. import urllib
  17. try:
  18. import imaplib2 as imaplib
  19. HAVE_IDLE = True
  20. except ImportError:
  21. import imaplib
  22. HAVE_IDLE = False
  23. import mad
  24. import mutagen.easyid3
  25. import yaml
  26. CONFIG_NAMES = ["~/.config/hotline.yaml", "/etc/hotline.yaml"]
  27. fn_filter = re.compile('wav|mp3|ogg', re.I)
  28. mutagen.easyid3.EasyID3.RegisterTXXXKey('ardj', 'ardj')
  29. def log_error(e, message):
  30. message += "\n" + traceback.format_exc(e)
  31. for line in message.strip().split("\n"):
  32. logging.error(line)
  33. def config_get(key, default=None):
  34. """Returns a value from the config file. The file is read on every call,
  35. which is OK because the traffic is unlikely that heavy, and you get instand
  36. updates without reloading the daemon."""
  37. for fn in CONFIG_NAMES:
  38. fn = os.path.expanduser(fn)
  39. if os.path.exists(fn):
  40. data = yaml.load(file(fn, "rb").read())
  41. return data.get(key, default)
  42. return default
  43. def install_syslog():
  44. """Makes use of the syslog."""
  45. logger = logging.getLogger()
  46. logger.setLevel(logging.DEBUG)
  47. syslog = logging.handlers.SysLogHandler(address="/dev/log")
  48. syslog.setLevel(logging.DEBUG)
  49. format_string = "hotline[%(process)d]: %(levelname)s %(message)s"
  50. formatter = logging.Formatter(format_string)
  51. syslog.setFormatter(formatter)
  52. logger.addHandler(syslog)
  53. def install_file_logger(filename):
  54. """Adds a custom formatter and a rotating file handler to the default
  55. logger."""
  56. folder = os.path.dirname(filename)
  57. if not os.path.exists(folder) or not os.access(folder, os.W_OK):
  58. raise Exception("Can't log to %s: no write permissions." % filename)
  59. max_size = 1000000
  60. max_count = 5
  61. logger = logging.getLogger()
  62. logger.setLevel(logging.DEBUG)
  63. h = logging.handlers.RotatingFileHandler(filename, maxBytes=max_size, backupCount=max_count)
  64. h.setFormatter(logging.Formatter('%(asctime)s - %(process)6d - %(levelname)s - %(message)s'))
  65. h.setLevel(logging.DEBUG)
  66. logger.addHandler(h)
  67. def message_has_audio(num, headers):
  68. """
  69. match = fn_filter.search(headers)
  70. if match is None:
  71. logging.debug("Message does not match: %s" % headers)
  72. return False
  73. """
  74. #logging.debug("Message %s has an audio file." % num)
  75. return True
  76. def run(cmd, wait=True):
  77. cmd.insert(0, "nice")
  78. cmd.insert(1, "-n15")
  79. logging.debug("Running a command: %s" % " ".join(cmd))
  80. tmp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  81. if wait:
  82. tmp.wait()
  83. def transcode(filename, body):
  84. logging.debug("Transcoding %s to MP3." % filename)
  85. tmp_name = wav_name = mp3_name = None
  86. try:
  87. tmp_name = tempfile.mktemp(suffix=os.path.splitext(filename)[1].lower())
  88. file(tmp_name, "wb").write(body)
  89. logging.debug("Wrote data to %s" % tmp_name)
  90. if tmp_name.endswith(".mp3"):
  91. mf = mad.MadFile(tmp_name)
  92. if mf.mode() != mad.MODE_SINGLE_CHANNEL and mf.samplerate() == 44100:
  93. logging.debug("File %s does not need transcoding." % filename)
  94. os.unlink(tmp_name)
  95. return body
  96. wav_name = tempfile.mktemp(suffix=".wav")
  97. run(["sox", "-q", tmp_name, "-r", "44100", "-c", "2", "-s", wav_name])
  98. logging.debug("Transcoded %s to %s" % (tmp_name, wav_name))
  99. mp3_name = tempfile.mktemp(suffix=".mp3")
  100. run(["lame", "--quiet", "--preset", "extreme", wav_name, mp3_name])
  101. run(["mp3gain", "-q", mp3_name])
  102. logging.debug("Transcoded %s to %s" % (wav_name, mp3_name))
  103. body = file(mp3_name, "rb").read()
  104. return body
  105. finally:
  106. if tmp_name and os.path.exists(tmp_name):
  107. os.unlink(tmp_name)
  108. if wav_name:
  109. os.unlink(wav_name)
  110. if mp3_name:
  111. os.unlink(mp3_name)
  112. def set_tags(filename, artist, date):
  113. tags = mutagen.easyid3.EasyID3()
  114. tags["artist"] = artist
  115. tags["title"] = time.strftime("%d.%m.%y %H:%M", date)
  116. tags["ardj"] = "ardj=1;labels=hotline"
  117. tags.save(filename)
  118. logging.debug("Wrote ID3 tags to %s" % filename)
  119. def mask_sender(name, addr, phone):
  120. if phone:
  121. phone_map = config_get("phone_map", {})
  122. return phone_map.get(phone, phone[:-7] + "XXX" + phone[-4:])
  123. addr_map = config_get("email_map", {})
  124. if addr in addr_map:
  125. return addr_map[addr]
  126. return name
  127. def process_file(name, body, sender_name, sender_addr, sender_phone, date):
  128. logging.debug("Incoming file: sender_name=%s sender_addr=%s sender_phone=%s" % (sender_name.encode("utf-8"), sender_addr.encode("utf-8"), sender_phone))
  129. sender = mask_sender(sender_name, sender_addr, sender_phone)
  130. logging.debug("%s (%s) sent a file: %s (%u bytes)" % (sender.encode("utf-8"), sender_addr.encode("utf-8"), name, len(body)))
  131. body = transcode(name, body)
  132. name = time.strftime(config_get("mp3_file_name", "/tmp/%Y-%m-%d-hotline-%H%M.mp3"), date)
  133. if os.path.exists(name):
  134. logging.warning("File %s already exists, skipping." % name)
  135. return False
  136. file(name, "wb").write(body)
  137. logging.info("Message from %s saved as %s" % (sender.encode("utf-8"), name))
  138. duration = int(mad.MadFile(name).total_time() / 1000)
  139. if duration < int(config_get("min_duration", 0)):
  140. logging.warning("Message is too short, skipped.")
  141. return False
  142. if duration > int(config_get("max_duration", 3600)):
  143. logging.warning("Message is too long, skipped.")
  144. return False
  145. set_tags(name, sender, date)
  146. page_name = time.strftime(config_get("page_name", "/tmp/%Y-%m-%d-hotline-%H%M.md"), date)
  147. if page_name:
  148. page = u"title: ??????? ?????: %(sender)s\ndate: %(date)s\nlabels: podcast, hotline\nfile: %(url)s\nfilesize: %(size)s\nduration: %(duration)u\n---\nNo description." % {
  149. "sender": sender,
  150. "date": time.strftime("%Y-%m-%d %H:%M:%S", date),
  151. "url": time.strftime(config_get("mp3_file_url", "http://example.com/files/%Y-%m-%d-hotline-%H%M.mp3"), date),
  152. "size": os.stat(name).st_size,
  153. "duration": duration,
  154. }
  155. page_dir = os.path.dirname(page_name)
  156. if not os.path.exists(page_dir):
  157. os.makedirs(page_dir)
  158. file(page_name, "wb").write(page.encode("utf-8"))
  159. logging.info("Wrote %s" % page_name)
  160. return True
  161. def decode_value(value):
  162. for part in re.findall("(=(?:\?[^?]+){3}\?=)", value):
  163. repl = u""
  164. for _t, _e in email.header.decode_header(part):
  165. if _e is None:
  166. repl += unicode(_e)
  167. else:
  168. repl += _t.decode(_e)
  169. value = value.replace(part, repl)
  170. return value
  171. def decode_file_name(encoded):
  172. """File names can contain all sorts of garbage, so we decode it, but only
  173. use the extension."""
  174. if encoded is not None:
  175. ext = decode_value(encoded).split(".")[-1]
  176. return "tmp." + ext
  177. def decode_sender(sender):
  178. """Decode UTF-8 base64 etc headers. decode_header() must be able to do
  179. this on its own, but due to a bug it fails to process multiline values."""
  180. sender = decode_value(sender)
  181. return rfc822.parseaddr(sender.replace("\r\n", ""))
  182. def get_phone_number(msg):
  183. """Extracts caller's phone number from the headers."""
  184. tmp = re.search("(\d+)", msg["X-WUM-FROM"] or "")
  185. if tmp:
  186. number = tmp.group(1)
  187. if number.startswith("8"):
  188. number = number[1:]
  189. if not number.startswith("+"):
  190. number = "+7" + number
  191. return number
  192. tmp = msg["X-Asterisk-CallerID"]
  193. if tmp:
  194. return tmp
  195. return None
  196. def download_message(mail, data):
  197. msg = email.message_from_string(data)
  198. logging.debug("Message-ID is %s" % msg["Message-ID"])
  199. sender_name, sender_addr = decode_sender(msg.get_all("From")[0])
  200. sender_phone = get_phone_number(msg)
  201. date = rfc822.parsedate(msg["Date"])
  202. status = False
  203. for part in msg.walk():
  204. if part.get_content_maintype() == "multipart":
  205. continue
  206. if part.get("Content-Disposition") is None:
  207. continue
  208. name = decode_file_name(part.get_filename())
  209. if name is None:
  210. logging.debug("Message part has no name, skipped.")
  211. continue
  212. data = part.get_payload(decode=True)
  213. if process_file(name, data, sender_name, sender_addr, sender_phone, date):
  214. status = True
  215. return status
  216. def check_one_message(mail, num):
  217. status = False
  218. logging.debug("Checking message %s." % num)
  219. result, data = mail.uid("fetch", num, "(BODY)")
  220. if result != "OK":
  221. return False
  222. if message_has_audio(num, data[0]):
  223. logging.debug("Fetching message %s" % num)
  224. result, data = mail.uid("fetch", num, "(RFC822)")
  225. if result == "OK":
  226. if download_message(mail, data[0][1]):
  227. status = True
  228. return status
  229. def search_messages(mail):
  230. have_new_messages = False
  231. logging.debug("Searching for new messages.")
  232. debug_num = config_get("imap_debug_message")
  233. if debug_num:
  234. message_ids = [str(debug_num)]
  235. else:
  236. result, data = mail.uid("search", "(UNSEEN)")
  237. if result != "OK":
  238. return False
  239. message_ids = data[0].split()
  240. for num in message_ids:
  241. try:
  242. if check_one_message(mail, num):
  243. have_new_messages = True
  244. except Exception, e:
  245. log_error(e, "Error checking message %s: %s" % (num, e))
  246. if have_new_messages:
  247. fn = config_get("postprocessor", "/bin/true")
  248. run(fn.split(" "), wait=False)
  249. else:
  250. logging.debug("No new messages were found.")
  251. def connect_and_wait():
  252. server = config_get("imap_server")
  253. logging.info("Connecting to %s" % server)
  254. mail = imaplib.IMAP4_SSL(server)
  255. mail.login(config_get("imap_user"), config_get("imap_password"))
  256. mail.select(config_get("imap_folder", "INBOX"))
  257. # Maybe there's something already.
  258. search_messages(mail)
  259. if config_get("imap_debug_message"):
  260. mail.logout()
  261. exit(0)
  262. while HAVE_IDLE:
  263. mail.idle()
  264. search_messages(mail)
  265. def loop():
  266. while True:
  267. try:
  268. connect_and_wait()
  269. except KeyboardInterrupt:
  270. logging.info("Interrupted by user.")
  271. return
  272. except Exception, e:
  273. log_error(e, "ERROR: %s, restarting in 5 seconds." % e)
  274. time.sleep(5)
  275. logging.debug("Sleeping for 60 seconds.")
  276. time.sleep(60)
  277. install_syslog()
  278. install_file_logger("/radio/logs/hotline.log")
  279. loop()