patreon.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. # encoding: utf-8
  2. from __future__ import unicode_literals
  3. import re
  4. from .common import InfoExtractor
  5. from ..utils import (
  6. ExtractorError,
  7. compat_html_parser,
  8. #compat_urllib_request,
  9. #compat_urllib_parse,
  10. )
  11. class PatreonHTMLParser(compat_html_parser.HTMLParser):
  12. _PREFIX = 'http://www.patreon.com'
  13. _ATTACH_TAGS = 5 * ['div']
  14. _ATTACH_CLASSES = [
  15. 'fancyboxhidden', 'box photo', 'boxwrapper',
  16. 'hiddendisplay shareinfo', 'attach'
  17. ]
  18. _INFO_TAGS = 4 * ['div']
  19. _INFO_CLASSES = [
  20. 'fancyboxhidden', 'box photo', 'boxwrapper',
  21. 'hiddendisplay shareinfo'
  22. ]
  23. def _match(self, attrs_classes, desired):
  24. if attrs_classes == desired:
  25. return True
  26. elif len(attrs_classes) == len(desired):
  27. return all(
  28. x.startswith(y)
  29. for x, y in zip(attrs_classes, desired)
  30. )
  31. return False
  32. def get_creation_info(self, html_data):
  33. self.tag_stack = []
  34. self.attrs_stack = []
  35. self.creation_info = {}
  36. self.feed(html_data)
  37. def handle_starttag(self, tag, attrs):
  38. self.tag_stack.append(tag.lower())
  39. self.attrs_stack.append(dict(attrs))
  40. def handle_endtag(self, tag):
  41. self.tag_stack.pop()
  42. self.attrs_stack.pop()
  43. def handle_data(self, data):
  44. # Check first if this is a creation attachment
  45. if self.tag_stack[-6:-1] == self._ATTACH_TAGS:
  46. attrs_classes = [
  47. x.get('class', '').lower() for x in self.attrs_stack[-6:-1]
  48. ]
  49. if self._match(attrs_classes, self._ATTACH_CLASSES):
  50. if self.tag_stack[-1] == 'a':
  51. url = self._PREFIX + self.attrs_stack[-1].get('href')
  52. self.creation_info['url'] = url
  53. if '.' in data:
  54. self.creation_info['ext'] = data.rsplit('.')[-1]
  55. # Next, check if this is within the div containing the creation info
  56. if self.tag_stack[-5:-1] == self._INFO_TAGS:
  57. attrs_classes = [
  58. x.get('class', '').lower() for x in self.attrs_stack[-5:-1]
  59. ]
  60. if self._match(attrs_classes, self._INFO_CLASSES):
  61. if self.attrs_stack[-1].get('class') == 'utitle':
  62. self.creation_info['title'] = data.strip()
  63. class PatreonIE(InfoExtractor):
  64. IE_NAME = 'patreon'
  65. _VALID_URL = r'https?://(?:www\.)?patreon\.com/creation\?hid=(.+)'
  66. _TESTS = [
  67. # CSS names with "double" in the name, i.e. "boxwrapper double"
  68. {
  69. 'url': 'http://www.patreon.com/creation?hid=743933',
  70. 'md5': 'e25505eec1053a6e6813b8ed369875cc',
  71. 'info_dict': {
  72. 'id': '743933',
  73. 'ext': 'mp3',
  74. 'title': 'Episode 166: David Smalley of Dogma Debate',
  75. 'uploader': 'Cognitive Dissonance Podcast',
  76. },
  77. },
  78. {
  79. 'url': 'http://www.patreon.com/creation?hid=754133',
  80. 'md5': '3eb09345bf44bf60451b8b0b81759d0a',
  81. 'info_dict': {
  82. 'id': '754133',
  83. 'ext': 'mp3',
  84. 'title': 'CD 167 Extra',
  85. 'uploader': 'Cognitive Dissonance Podcast',
  86. },
  87. },
  88. ]
  89. # Currently Patreon exposes download URL via hidden CSS, so login is not
  90. # needed. Keeping this commented for when this inevitably changes.
  91. '''
  92. def _login(self):
  93. (username, password) = self._get_login_info()
  94. if username is None:
  95. return
  96. login_form = {
  97. 'redirectUrl': 'http://www.patreon.com/',
  98. 'email': username,
  99. 'password': password,
  100. }
  101. request = compat_urllib_request.Request(
  102. 'https://www.patreon.com/processLogin',
  103. compat_urllib_parse.urlencode(login_form).encode('utf-8')
  104. )
  105. login_page = self._download_webpage(request, None, note='Logging in as %s' % username)
  106. if re.search(r'onLoginFailed', login_page):
  107. raise ExtractorError('Unable to login, incorrect username and/or password', expected=True)
  108. def _real_initialize(self):
  109. self._login()
  110. '''
  111. def _real_extract(self, url):
  112. mobj = re.match(self._VALID_URL, url)
  113. video_id = mobj.group(1)
  114. info_page = self._download_webpage(url, video_id)
  115. ret = {'id': video_id}
  116. try:
  117. ret['uploader'] = re.search(
  118. r'<strong>(.+)</strong> is creating', info_page
  119. ).group(1)
  120. except AttributeError:
  121. pass
  122. parser = PatreonHTMLParser()
  123. parser.get_creation_info(info_page)
  124. if not parser.creation_info.get('url'):
  125. raise ExtractorError('Unable to retrieve creation URL')
  126. ret.update(parser.creation_info)
  127. return ret