yt_noapi.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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):
  30. self.type = ""
  31. self.title = ""
  32. self.videos = []
  33. class YtVideo(object):
  34. """Stores the relevant attributes of a single youtube video."""
  35. def __init__(self, title, ytid):
  36. self.title = title
  37. self.ytid = ytid
  38. class Connector(object):
  39. """This class will retrieve all the necessary data from youtube using
  40. youtube-dl, thus bypassing the API.
  41. """
  42. def __init__(self, subscription_type, name, conf, search=""):
  43. """Populates the object with all necessary data."""
  44. self._type = subscription_type
  45. self._name = name
  46. self._search = search
  47. self._conf = conf
  48. self._title = ""
  49. self._url = ""
  50. self._construct_url()
  51. def _construct_url(self):
  52. """Constructs the URL to request from youtube-dl according to the
  53. subscription type and the given parameters.
  54. """
  55. if self._type == "user":
  56. self._url = "https://www.youtube.com/user/%s" \
  57. % urllib.parse.quote(self._name)
  58. elif self._type == "channel":
  59. self._url = "https://www.youtube.com/channel/%s" \
  60. % urllib.parse.quote(self._name)
  61. elif self._type == "search":
  62. self._url = "https://www.youtube.com/results?search_query=%s"\
  63. % urllib.parse.quote(self._search)
  64. elif self._type == "playlist":
  65. self._url = "https://www.youtube.com/playlist?list=%s" \
  66. % urllib.parse.quote(self._name)
  67. LOGGER.debug(_("Constructed URL for subscription: %s"), self._url)
  68. def _fetch_title(self):
  69. """Retrieves the title of the HTML page to use as a title for the
  70. subscription."""
  71. try:
  72. LOGGER.debug(_("Opening URL: %s to fetch title"), self._url)
  73. data = urllib.request.urlopen(self._url)
  74. except urllib.error.HTTPError as err:
  75. if err.code == 404 and self._type == "user":
  76. self._type = "channel"
  77. self._construct_url()
  78. try:
  79. data = urllib.request.urlopen(self._url)
  80. except urllib.error.HTTPError:
  81. raise stov_exceptions.ChannelNotFound()
  82. else:
  83. self._parse_title(data)
  84. else:
  85. raise stov_exceptions.ChannelNotFound()
  86. else:
  87. self._parse_title(data)
  88. def _parse_title(self, data):
  89. """ Parses the title from a HTML document
  90. :param data: HTTP connection to the document
  91. :type data: http.client.HTTPResponse
  92. """
  93. parsed_html = lxml.html.parse(data)
  94. data.close()
  95. i = 0
  96. for item in parsed_html.iter("title"):
  97. if i == 0:
  98. self._title = item.text_content().strip().replace("\n", "")
  99. i += 1
  100. if self._search != "" and self._type == "user":
  101. self._title += _(" search %s") % self._search
  102. def _fetch_videos(self, existing_videos):
  103. """Retrieves all the relevant videos in a subscription."""
  104. videos_list = []
  105. if self._type == "user" and self._search:
  106. video_ids = youtubedl_wrapper.get_ids(self._conf, self._url,
  107. self._search)
  108. else:
  109. video_ids = youtubedl_wrapper.get_ids(self._conf, self._url)
  110. LOGGER.debug("Got video IDs: %s", video_ids)
  111. if video_ids:
  112. for video_id in video_ids:
  113. video_exists = False
  114. if existing_videos:
  115. for existing_video in existing_videos:
  116. if video_id == existing_video.ytid:
  117. video_exists = True
  118. break
  119. if not video_exists:
  120. try:
  121. video_title = youtubedl_wrapper.get_title(
  122. self._conf, "https://www.youtube.com/watch?v=%s"
  123. % video_id)
  124. except subprocess.CalledProcessError:
  125. raise stov_exceptions.YoutubeDlCallFailed()
  126. else:
  127. videos_list.append(YtVideo(
  128. video_title,
  129. video_id))
  130. return videos_list
  131. def parse_api_data(self, existing_videos):
  132. """This method calls all necessary methods to retrieve the data
  133. and assembles them into a Channel object. The naming of this
  134. method was set according to the method in youtubeAPI to be
  135. compatible.
  136. """
  137. self._fetch_title()
  138. videos = self._fetch_videos(existing_videos)
  139. channel = YtChannel()
  140. channel.title = self._title
  141. channel.videos = videos
  142. channel.type = self._type
  143. return channel