yt_noapi.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. #
  2. # This file is part of stov, written by Helmut Pozimski 2012-2017.
  3. #
  4. # stov is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, version 2 of the License.
  7. #
  8. # stov is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with stov. If not, see <http://www.gnu.org/licenses/>.
  15. # -*- coding: utf8 -*-
  16. """This module provides all classes and methods that were provided by the
  17. youtubeAPI module earlier."""
  18. import subprocess
  19. import urllib.parse
  20. import urllib.request
  21. import urllib.error
  22. import logging
  23. import lxml.html
  24. from lib_stov import stov_exceptions
  25. from lib_stov import youtubedl_wrapper
  26. LOGGER = logging.getLogger("stov")
  27. class YtChannel(object):
  28. """Stores the relevant attributes of a youtube channel."""
  29. def __init__(self, _type, title, videos=None):
  30. if videos is None:
  31. videos = []
  32. self.type = _type
  33. self.title = title
  34. self.videos = videos
  35. class YtVideo(object):
  36. """Stores the relevant attributes of a single youtube video."""
  37. def __init__(self, title, ytid):
  38. self.title = title
  39. self.video_id = ytid
  40. class Connector(object):
  41. """This class will retrieve all the necessary data from youtube using
  42. youtube-dl, thus bypassing the API.
  43. """
  44. def __init__(self, subscription_type, name, conf, search=""):
  45. """Populates the object with all necessary data."""
  46. self._type = subscription_type
  47. self._name = name
  48. self._search = search
  49. self._conf = conf
  50. self._title = ""
  51. self._url = ""
  52. self._construct_url()
  53. def _construct_url(self):
  54. """Constructs the URL to request from youtube-dl according to the
  55. subscription type and the given parameters.
  56. """
  57. if self._type == "user":
  58. self._url = "https://www.youtube.com/user/%s" \
  59. % urllib.parse.quote(self._name)
  60. elif self._type == "channel":
  61. self._url = "https://www.youtube.com/channel/%s" \
  62. % urllib.parse.quote(self._name)
  63. elif self._type == "search":
  64. self._url = "https://www.youtube.com/results?search_query=%s"\
  65. % urllib.parse.quote(self._search)
  66. elif self._type == "playlist":
  67. self._url = "https://www.youtube.com/playlist?list=%s" \
  68. % urllib.parse.quote(self._name)
  69. LOGGER.debug(_("Constructed URL for subscription: %s"), self._url)
  70. def _fetch_title(self):
  71. """Retrieves the title of the HTML page to use as a title for the
  72. subscription."""
  73. try:
  74. LOGGER.debug(_("Opening URL: %s to fetch title"), self._url)
  75. data = urllib.request.urlopen(self._url)
  76. except urllib.error.HTTPError as err:
  77. if err.code == 404 and self._type == "user":
  78. self._type = "channel"
  79. self._construct_url()
  80. try:
  81. data = urllib.request.urlopen(self._url)
  82. except urllib.error.HTTPError:
  83. raise stov_exceptions.ChannelNotFound()
  84. else:
  85. self._parse_title(data)
  86. else:
  87. raise stov_exceptions.ChannelNotFound()
  88. else:
  89. self._parse_title(data)
  90. def _parse_title(self, data):
  91. """ Parses the title from a HTML document
  92. :param data: HTTP connection to the document
  93. :type data: http.client.HTTPResponse
  94. """
  95. parsed_html = lxml.html.parse(data)
  96. data.close()
  97. i = 0
  98. for item in parsed_html.iter("title"):
  99. if i == 0:
  100. self._title = item.text_content().strip().replace("\n", "")
  101. i += 1
  102. if self._search != "" and self._type == "user":
  103. self._title += _(" search %s") % self._search
  104. def _fetch_videos(self, existing_videos):
  105. """Retrieves all the relevant videos in a subscription."""
  106. videos_list = []
  107. if self._type == "user" and self._search:
  108. video_ids = youtubedl_wrapper.get_ids(self._conf, self._url,
  109. self._search)
  110. elif self._type == "playlist":
  111. video_ids = youtubedl_wrapper.get_ids(self._conf, self._url,
  112. reverse=True)
  113. else:
  114. video_ids = youtubedl_wrapper.get_ids(self._conf, self._url)
  115. LOGGER.debug("Got video IDs: %s", video_ids)
  116. if video_ids:
  117. for video_id in video_ids:
  118. video_exists = False
  119. if existing_videos:
  120. for existing_video in existing_videos:
  121. if video_id == existing_video.site_id:
  122. video_exists = True
  123. break
  124. if not video_exists:
  125. try:
  126. video_title = youtubedl_wrapper.get_title(
  127. self._conf, self.construct_video_url(video_id))
  128. except subprocess.CalledProcessError:
  129. raise stov_exceptions.YoutubeDlCallFailed()
  130. else:
  131. videos_list.append(YtVideo(
  132. video_title,
  133. video_id))
  134. return videos_list
  135. def parse_api_data(self, existing_videos):
  136. """This method calls all necessary methods to retrieve the data
  137. and assembles them into a Channel object. The naming of this
  138. method was set according to the method in youtubeAPI to be
  139. compatible.
  140. """
  141. self._fetch_title()
  142. videos = self._fetch_videos(existing_videos)
  143. channel = YtChannel(self._type, self._title, videos)
  144. return channel
  145. @staticmethod
  146. def construct_video_url(ytid):
  147. """
  148. Resturns the URL to a specified youtube video
  149. :param ytid: Youtube ID of the video
  150. :type ytid: str
  151. :return: Video URL
  152. :rtype: str
  153. """
  154. url = "https://www.youtube.com/watch?v=%s" % ytid
  155. return url
  156. @staticmethod
  157. def get_quality_parameter(config):
  158. """Determines which itag value results from codec and resolution
  159. settings and returns it
  160. :param config: configuration object
  161. :type config: lib_stov.configuration.Conf
  162. :return: itag value
  163. :rtype: str
  164. """
  165. LOGGER.debug(_("Trying to determine the itag value for youtube-dl from"
  166. " your quality and codec settings."))
  167. itag_value = 0
  168. if config.values["videocodec"] == "flv":
  169. if config.values["maxresolution"] == "240p":
  170. itag_value = 5
  171. elif config.values["maxresolution"] == "270p":
  172. itag_value = 6
  173. elif config.values["maxresolution"] == "360p":
  174. itag_value = 34
  175. elif config.values["maxresolution"] == "480p":
  176. itag_value = 35
  177. elif config.values["videocodec"] == "webm":
  178. if config.values["maxresolution"] == "360p":
  179. itag_value = 43
  180. elif config.values["maxresolution"] == "480p":
  181. itag_value = 44
  182. elif config.values["maxresolution"] == "720p":
  183. itag_value = 45
  184. elif config.values["maxresolution"] == "1080p":
  185. itag_value = 46
  186. elif config.values["videocodec"] == "mp4":
  187. if config.values["maxresolution"] == "360p":
  188. itag_value = 18
  189. elif config.values["maxresolution"] == "720p":
  190. itag_value = 22
  191. elif config.values["maxresolution"] == "1080p":
  192. itag_value = 37
  193. elif config.values["maxresolution"] == "3072p":
  194. itag_value = 38
  195. if itag_value:
  196. LOGGER.debug(_("Found value: %s."), itag_value)
  197. return str(itag_value) + "/" + config.values["videocodec"]
  198. else:
  199. LOGGER.debug(_("Could not determine an itag value "
  200. "from the configuration"))
  201. return "38"