yt_noapi.py 5.7 KB

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