PageRenderTime 30ms CodeModel.GetById 38ms RepoModel.GetById 1ms app.codeStats 0ms

/server/handlers.py

https://github.com/feross/Instant.fm
Python | 512 lines | 460 code | 46 blank | 6 comment | 30 complexity | 1251c01b82f594108c57a4e27630ecfc MD5 | raw file
  1. import os
  2. import re
  3. import io
  4. import sys
  5. import tornado.web
  6. import bcrypt
  7. from PIL import Image
  8. import urllib2
  9. import hashlib
  10. import sqlalchemy
  11. import validation
  12. import utils
  13. import model
  14. class UnsupportedFormatException(Exception): pass
  15. class HandlerBase(tornado.web.RequestHandler):
  16. """ All handlers should extend this """
  17. def __init__(self, application, request, **kwargs):
  18. super(HandlerBase, self).__init__(application, request, **kwargs)
  19. self.db_session = model.DbSession()
  20. # Cache the session and user
  21. self._current_session = None
  22. self._current_user = None
  23. self.xsrf_token # Sets token
  24. def write_error(self, status_code, **kwargs):
  25. """Renders error pages (called internally by Tornado)"""
  26. if status_code == 404:
  27. self.render(os.path.join(sys.path[0], '../static/404.html'))
  28. def get_current_user(self):
  29. if self._current_user is not None:
  30. return self._current_user
  31. self._current_user = self.get_current_session().user
  32. return self._current_user
  33. def get_current_session(self):
  34. if self._current_session is not None:
  35. return self._current_session
  36. session_id = self.get_secure_cookie('session_id')
  37. if session_id:
  38. self._current_session = (self.db_session.query(model.Session)
  39. .get(session_id))
  40. if self._current_session == None:
  41. self._current_session = model.Session()
  42. self.db_session.add(self._current_session)
  43. self.db_session.flush()
  44. print("Setting session cookie: " + str(self._current_session.id))
  45. self.set_secure_cookie('session_id', str(self._current_session.id))
  46. return self._current_session
  47. def clear_old_sessions(self):
  48. self.db_session.execute("DELETE FROM sessions WHERE `create_date` < CURDATE() - 30")
  49. self.db_session.flush()
  50. def get_profile_url(self):
  51. user = self.get_current_user()
  52. return '/user/' + user.profile if user is not None else ''
  53. def owns_playlist(self, playlist):
  54. if playlist is None:
  55. return False
  56. session = self.get_current_session()
  57. user = self.get_current_user()
  58. # The Feross Exception
  59. if user is not None and user.id == 1:
  60. return True
  61. return ((session.id is not None and str(session.id) == str(playlist.session_id))
  62. or (user is not None and str(user.id) == str(playlist.user_id)))
  63. def get_playlists_for_current_user(self):
  64. query = self.db_session.query(model.Playlist)
  65. if self.get_current_user() is not None:
  66. query = query.filter_by(user_id=self.get_current_user().id)
  67. else:
  68. query = query.filter_by(session_id=self.get_current_session().id)
  69. query = query.order_by(model.Playlist.id.desc())
  70. return query.all()
  71. def _log_user_in(self, user, expire_on_browser_close=False):
  72. # Promote playlists, uploaded images, and session to be owned by user
  73. session = self.get_current_session()
  74. (self.db_session.query(model.Playlist)
  75. .filter_by(session_id=session.id, user_id=None)
  76. .update({"user_id": user.id}))
  77. (self.db_session.query(model.Image)
  78. .filter_by(session_id=session.id, user_id=None)
  79. .update({"user_id": user.id}))
  80. session.user_id = user.id
  81. self.db_session.flush()
  82. return user.client_visible_attrs
  83. def _log_user_out(self):
  84. session_id = self.get_secure_cookie('session_id')
  85. self.clear_cookie('session_id')
  86. self._current_session = None
  87. if session_id:
  88. (self.db_session.query(model.Session)
  89. .filter_by(id=session_id)
  90. .delete())
  91. # New session
  92. new_session = model.Session();
  93. self.db_session.add(new_session)
  94. self.db_session.flush()
  95. self.set_secure_cookie('session_id', str(new_session.id))
  96. return new_session
  97. class PlaylistHandlerBase(HandlerBase):
  98. """ Any handler that involves playlists should extend this.
  99. """
  100. def _render_playlist_view(self, template_name, playlist=None, title=None, share=None, **kwargs):
  101. if title is None and playlist is not None:
  102. title = playlist.title
  103. self.render(template_name, playlist=playlist, share=share, title=title, **kwargs)
  104. class UserHandlerBase(HandlerBase):
  105. def _verify_password(self, password, hashed):
  106. return bcrypt.hashpw(password, hashed) == hashed
  107. def _hash_password(self, password):
  108. return bcrypt.hashpw(password, bcrypt.gensalt())
  109. def _is_registered_fbid(self, fb_id):
  110. return self.db_session.query(model.User).filter_by(fb_id=fb_id).count() > 0
  111. class ImageHandlerBase(HandlerBase):
  112. STATIC_DIR = 'static'
  113. IMAGE_DIR = '/images/uploaded/'
  114. def _crop_to_square(self, image):
  115. cropped_side_length = min(image.size)
  116. square = ((image.size[0] - cropped_side_length) / 2,
  117. (image.size[1] - cropped_side_length) / 2,
  118. (image.size[0] + cropped_side_length) / 2,
  119. (image.size[1] + cropped_side_length) / 2)
  120. return image.crop(square)
  121. def _resize(self, image, side_length):
  122. image = image.copy()
  123. size = (side_length, side_length)
  124. image.thumbnail(size, Image.ANTIALIAS)
  125. return image
  126. def _save_image(self, image_id, image_format, image):
  127. filename = '{0:x}-{1}x{2}.{3}'.format(image_id,
  128. image.size[0],
  129. image.size[1],
  130. image_format.lower())
  131. path = os.path.join(self.IMAGE_DIR, filename)
  132. image.save(self.STATIC_DIR + path, img_format=image.format)
  133. return path
  134. def _is_valid_image(self, data):
  135. try:
  136. image = Image.open(data)
  137. image.verify()
  138. except:
  139. return False
  140. data.seek(0)
  141. return True
  142. def _handle_image(self, data, playlist_id):
  143. result = {'status': 'OK', 'images': {}}
  144. if self._is_valid_image(data) == False:
  145. result['status'] = 'No valid image at that URL.'
  146. return result
  147. image = model.Image()
  148. image.user = self.get_current_user()
  149. image.session = self.get_current_session()
  150. self.db_session.add(image)
  151. playlist = self.db_session.query(model.Playlist).get(playlist_id)
  152. playlist.image = image
  153. self.db_session.commit()
  154. original_image = Image.open(data)
  155. cropped_image = self._crop_to_square(original_image)
  156. image.original = self._save_image(image.id, original_image.format, original_image)
  157. image.medium = self._save_image(image.id, original_image.format, self._resize(cropped_image, 160))
  158. self.db_session.commit()
  159. class HomeHandler(HandlerBase):
  160. def get(self):
  161. categories = []
  162. used_playlist_ids = set()
  163. my_lists_title = ('My Playlists' if self.get_current_user() is None
  164. else self.get_current_user().name + "'s Playlists")
  165. categories.append({
  166. 'title': my_lists_title,
  167. 'playlists': self.get_playlists_for_current_user()
  168. })
  169. staff_picks = (self.db_session.query(model.Playlist)
  170. .filter_by(featured=True)
  171. .order_by(model.Playlist.views.desc())
  172. .limit(8)
  173. .all())
  174. categories.append({
  175. 'title': 'Staff Picks',
  176. 'playlists': staff_picks,
  177. })
  178. used_playlist_ids |= set([playlist.id for playlist in staff_picks])
  179. categories.append({
  180. 'title': 'Popular',
  181. 'playlists': (self.db_session.query(model.Playlist)
  182. .filter(model.Playlist._songs != '[]')
  183. .filter(model.Playlist.user_id != None)
  184. .filter(model.Playlist.session_id != None)
  185. .filter(model.Playlist.hide == 0)
  186. .filter(sqlalchemy.not_((model.Playlist.id.in_(used_playlist_ids))))
  187. .order_by(model.Playlist.views.desc())
  188. .limit(16)
  189. .all()
  190. )})
  191. self.render("index.html",
  192. title="Instant.fm - Share Music Instantly",
  193. categories=categories)
  194. class TermsHandler(HandlerBase):
  195. def get(self):
  196. self.render("terms.html")
  197. class MaintenanceHandler(HandlerBase):
  198. def get(self):
  199. self.clear_old_sessions()
  200. self.render("maintenance.html")
  201. class PlaylistHandler(PlaylistHandlerBase):
  202. """Landing page for a playlist"""
  203. def get(self, playlist_alpha_id):
  204. playlist_id = utils.base36_10(playlist_alpha_id)
  205. playlist = self.db_session.query(model.Playlist).get(playlist_id)
  206. if playlist is None:
  207. return self.send_error(404)
  208. playlist.views += 1
  209. if self.get_argument('json', default=False):
  210. self.write(playlist.json())
  211. else:
  212. self._render_playlist_view('playlist.html', playlist=playlist)
  213. class SearchHandler(PlaylistHandlerBase):
  214. # TODO: Make this actually work.
  215. def get(self):
  216. self._render_playlist_view('search.html')
  217. class ArtistHandler(PlaylistHandlerBase):
  218. """ Renders an empty artist template """
  219. def get(self, requested_artist_name):
  220. artist_name = utils.deurlify(requested_artist_name)
  221. self._render_playlist_view('artist.html',
  222. artist_name=artist_name)
  223. class AlbumHandler(PlaylistHandlerBase):
  224. def get(self, requested_artist_name, requested_album_name):
  225. """ Renders an empty album template """
  226. artist_name = utils.deurlify(requested_artist_name)
  227. album_name = utils.deurlify(requested_album_name)
  228. self._render_playlist_view('album.html',
  229. artist_name=artist_name,
  230. album_name=album_name)
  231. class SongHandler(PlaylistHandlerBase):
  232. def get(self, requested_artist_name, requested_song_name):
  233. """ Renders an empty album template """
  234. share = None
  235. artist_name = utils.deurlify(requested_artist_name)
  236. song_name = utils.deurlify(requested_song_name)
  237. yt = self.get_argument('yt', None)
  238. img = self.get_argument('img', None)
  239. if yt is not None and img is not None:
  240. share = {'yt': yt,
  241. 'img': img}
  242. self._render_playlist_view('song.html',
  243. artist_name=artist_name,
  244. song_name=song_name,
  245. title = song_name + ' by ' + artist_name,
  246. share=share)
  247. class UploadHandler(PlaylistHandlerBase):
  248. """ Handles playlist upload requests """
  249. def _has_uploaded_files(self):
  250. files = self.request.files
  251. if 'file' not in files or len(files['file']) == 0:
  252. return False
  253. return True
  254. def _get_request_content(self):
  255. file = self.request.files['file'][0]
  256. filename = file['filename']
  257. contents = file['body']
  258. return (filename, contents)
  259. def _parseM3U(self, contents):
  260. f = io.StringIO(contents.decode('utf-8'), newline=None)
  261. first_line = f.readline()
  262. if not re.match(r"#EXTM3U", first_line):
  263. return None
  264. # Attempt to guess if the artist/title are in iTunes order
  265. itunes_format = False
  266. while True:
  267. line = f.readline()
  268. if len(line) == 0:
  269. break
  270. if re.match(r"[^#].*([/\\])iTunes\1", line):
  271. itunes_format = True
  272. break
  273. f.seek(0)
  274. res_arr = []
  275. while True:
  276. line = f.readline()
  277. if len(line) == 0:
  278. break
  279. line = line.rstrip("\n")
  280. if itunes_format:
  281. res = re.match(r"#EXTINF:\d*,(.*) - (.*)", line)
  282. if res:
  283. title = res.group(1)
  284. artist = res.group(2)
  285. res_arr.append({'t': title, 'a': artist})
  286. else:
  287. # Slightly different regex to handle dashes in song titles better
  288. res = re.match(r"#EXTINF:\d*,(.*?) - (.*)", line)
  289. if res:
  290. artist = res.group(1)
  291. title = res.group(2)
  292. res_arr.append({'t': title, 'a': artist})
  293. return res_arr
  294. def _parse_text(self, contents):
  295. try:
  296. decoded = contents.decode('utf-8')
  297. except:
  298. decoded = contents.decode('utf-16')
  299. f = io.StringIO(decoded, newline=None)
  300. first_line = f.readline()
  301. if not re.match(r"Name\tArtist", first_line):
  302. return None
  303. res_arr = []
  304. while True:
  305. line = f.readline()
  306. if len(line) == 0:
  307. break
  308. line = line.rstrip("\n")
  309. res = re.match(r"([^\t]*)\t([^\t]*)", line)
  310. if res:
  311. title = res.group(1)
  312. artist = res.group(2)
  313. res_arr.append({'t': title, 'a': artist})
  314. return res_arr
  315. def _parse_pls(self, contents):
  316. f = io.StringIO(contents.decode('utf-8'), newline=None)
  317. first_line = f.readline()
  318. if not re.match(r"\[playlist\]", first_line):
  319. return None
  320. res_arr = []
  321. while True:
  322. line = f.readline()
  323. if len(line) == 0:
  324. break
  325. line = line.rstrip("\n")
  326. res = re.match(r"Title\d=(.*?) - (.*)", line)
  327. if res:
  328. artist = res.group(1)
  329. title = res.group(2)
  330. res_arr.append({'t': title, 'a': artist})
  331. return res_arr
  332. def _parse_songs_from_uploaded_file(self):
  333. (filename, contents) = self._get_request_content()
  334. ext = os.path.splitext(filename)[1]
  335. # Parse the file based on the format
  336. songs = None
  337. if ext == ".m3u" or ext == ".m3u8":
  338. songs = self._parseM3U(contents)
  339. elif ext == ".txt":
  340. songs = self._parse_text(contents)
  341. elif ext == ".pls":
  342. songs = self._parse_pls(contents)
  343. if songs is None:
  344. raise(UnsupportedFormatException())
  345. return songs
  346. @validation.validated
  347. def post(self):
  348. """ Handles the "New Playlist" form post.
  349. This can't be JSON RPC because of the file uploading.
  350. """
  351. validator = validation.Validator(immediate_exceptions=True)
  352. title = self.get_argument('title', default='', strip=True)
  353. validator.add_rule(title, 'Title', min_length=1)
  354. description = self.get_argument('description', default=None, strip=True)
  355. songs = []
  356. if self._has_uploaded_files():
  357. try:
  358. songs = self._parse_songs_from_uploaded_file()
  359. except UnsupportedFormatException:
  360. validator.error('Unsupported format.')
  361. playlist = model.Playlist(title)
  362. playlist.description = description
  363. playlist.songs = songs
  364. playlist.session = self.get_current_session()
  365. playlist.user = self.get_current_user()
  366. self.db_session.add(playlist)
  367. self.db_session.flush()
  368. self.set_header("Content-Type", "application/json")
  369. return playlist.client_visible_attrs;
  370. class ProfileHandler(PlaylistHandlerBase):
  371. def get(self, requested_user_name):
  372. self._render_playlist_view('profile.html', playlist=None)
  373. class TTSHandler(PlaylistHandlerBase):
  374. q = None
  375. @tornado.web.asynchronous
  376. def get(self):
  377. self.q = self.get_argument("q")
  378. self.set_header("Content-Type", "audio/mpeg")
  379. q_encoded = urllib2.quote(self.q.encode("utf-8"))
  380. url = "http://translate.google.com/translate_tts?q=" + q_encoded + "&tl=en"
  381. http = tornado.httpclient.AsyncHTTPClient()
  382. http.fetch(url, callback=self.on_response)
  383. def on_response(self, response):
  384. if response.error: raise tornado.web.HTTPError(500)
  385. filename = hashlib.sha1(self.q.encode('utf-8')).hexdigest()
  386. fileObj = open(os.path.join(os.path.dirname(__file__), "../static/tts/" + filename + ".mp3"), "w")
  387. fileObj.write(response.body)
  388. fileObj.close()
  389. self.write(response.body)
  390. self.finish()
  391. class ErrorHandler(HandlerBase):
  392. def prepare(self):
  393. self.send_error(404)