nficano commited on
Commit
4eb9602
·
1 Parent(s): 193f9f8

Added better exception handling, fixed a ton of bugs, added setup.py.

Browse files
Files changed (5) hide show
  1. README.md +43 -38
  2. TODO +0 -12
  3. youtube/youtube.py → pytube/__init__.py +71 -41
  4. setup.py +34 -0
  5. youtube/__init__.py +0 -0
README.md CHANGED
@@ -29,18 +29,13 @@ The only features I see implementing in the near future are:
29
  - Allow it to run as a command-line utility.
30
  - Making it compatible with Python 3.
31
 
32
- ### Known bugs
33
- - "Multiple videos returned" gets raised to frequently due to the lack
34
- of codec/quality information I've mapped to the fmt code in the TT_ENCODING
35
- dict. For more info see: [Wikipedia - YouTube Quality and codecs](http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs)
36
-
37
  ## Usage Example
38
 
39
  ``` python
40
  from youtube import YouTube
41
 
42
  # not necessary, just for demo purposes
43
- from pprint import pprint as pp
44
 
45
  yt = YouTube()
46
 
@@ -50,26 +45,17 @@ yt.url = "http://www.youtube.com/watch?v=Ik-RsDGPI5Y"
50
  # Once set, you can see all the codec and quality options YouTube has made
51
  # available for the perticular video by printing videos.
52
 
53
- pp(yt.videos)
54
-
55
- #[<Video: 3gp - 144p>,
56
- # <Video: 3gp - 144p>,
57
- # <Video: 3gp - 240p>,
58
- # <Video: 3gp - 240p>,
59
- # <Video: flv - 224p>,
60
- # <Video: flv - 224p>,
61
- # <Video: flv - 360p>,
62
- # <Video: flv - 360p>,
63
- # <Video: flv - 480p>,
64
- # <Video: flv - 480p>,
65
- # <Video: mp4 - 360p>,
66
- # <Video: mp4 - 360p>,
67
- # <Video: mp4 - 720p>,
68
- # <Video: mp4 - 720p>,
69
- # <Video: webm - 360p>,
70
- # <Video: webm - 360p>,
71
- # <Video: webm - 480p>,
72
- # <Video: webm - 480p>]
73
 
74
  # The filename is automatically generated based on the video title.
75
  # You can override this by manually setting the filename.
@@ -84,28 +70,47 @@ yt.filename = 'Dancing Scene from Pulp Fiction'
84
 
85
  # You can also filter the criteria by filetype.
86
 
87
- pp(yt.filter('flv'))
88
 
89
- # [<Video: flv - 224p>,
90
- # <Video: flv - 224p>,
91
- # <Video: flv - 360p>,
92
- # <Video: flv - 360p>,
93
- # <Video: flv - 480p>,
94
- # <Video: flv - 480p>]
95
 
96
  # and by resolution
97
- pp(yt.filter(res='480p'))
98
 
99
- # [<Video: flv - 480p>,
100
- # <Video: flv - 480p>,
101
- # <Video: webm - 480p>,
102
- # <Video: webm - 480p>]
103
 
104
  # to select a video by a specific resolution and filetype you can use the get
105
  # method.
106
 
107
  video = yt.get('mp4', '720p')
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  # Okay, let's download it!
110
  video.download()
111
 
 
29
  - Allow it to run as a command-line utility.
30
  - Making it compatible with Python 3.
31
 
 
 
 
 
 
32
  ## Usage Example
33
 
34
  ``` python
35
  from youtube import YouTube
36
 
37
  # not necessary, just for demo purposes
38
+ from pprint import pprint
39
 
40
  yt = YouTube()
41
 
 
45
  # Once set, you can see all the codec and quality options YouTube has made
46
  # available for the perticular video by printing videos.
47
 
48
+ pprint(yt.videos)
49
+
50
+ #[<Video: MPEG-4 Visual (.3gp) - 144p>,
51
+ # <Video: MPEG-4 Visual (.3gp) - 240p>,
52
+ # <Video: Sorenson H.263 (.flv) - 240p>,
53
+ # <Video: H.264 (.flv) - 360p>,
54
+ # <Video: H.264 (.flv) - 480p>,
55
+ # <Video: H.264 (.mp4) - 360p>,
56
+ # <Video: H.264 (.mp4) - 720p>,
57
+ # <Video: VP8 (.webm) - 360p>,
58
+ # <Video: VP8 (.webm) - 480p>]
 
 
 
 
 
 
 
 
 
59
 
60
  # The filename is automatically generated based on the video title.
61
  # You can override this by manually setting the filename.
 
70
 
71
  # You can also filter the criteria by filetype.
72
 
73
+ pprint(yt.filter('flv'))
74
 
75
+ #[<Video: Sorenson H.263 (.flv) - 240p>,
76
+ # <Video: H.264 (.flv) - 360p>,
77
+ # <Video: H.264 (.flv) - 480p>]
 
 
 
78
 
79
  # and by resolution
80
+ pprint(yt.filter(res='480p'))
81
 
82
+ #[<Video: H.264 (.flv) - 480p>,
83
+ #<Video: VP8 (.webm) - 480p>]
 
 
84
 
85
  # to select a video by a specific resolution and filetype you can use the get
86
  # method.
87
 
88
  video = yt.get('mp4', '720p')
89
 
90
+ # NOTE: get() can only be used if and only if one object matches your criteria.
91
+ # for example:
92
+
93
+ pprint(yt.videos)
94
+
95
+ #[<Video: MPEG-4 Visual (.3gp) - 144p>,
96
+ # <Video: MPEG-4 Visual (.3gp) - 240p>,
97
+ # <Video: Sorenson H.263 (.flv) - 240p>,
98
+ # <Video: H.264 (.flv) - 360p>,
99
+ # <Video: H.264 (.flv) - 480p>,
100
+ # <Video: H.264 (.mp4) - 360p>,
101
+ # <Video: H.264 (.mp4) - 720p>,
102
+ # <Video: VP8 (.webm) - 360p>,
103
+ # <Video: VP8 (.webm) - 480p>]
104
+
105
+ # Notice we have two H.264 (.mp4) available to us.. now if we try to call get()
106
+ # on mp4..
107
+
108
+ video = yt.get('mp4')
109
+ # MultipleObjectsReturned: get() returned more than one object -- it returned 2!
110
+
111
+ # In this case, we'll need to specify both the codec (mp4) and resolution
112
+ # (either 360p or 720p).
113
+
114
  # Okay, let's download it!
115
  video.download()
116
 
TODO DELETED
@@ -1,12 +0,0 @@
1
- TODO
2
- ====
3
-
4
- * Refactor filename out of ``Video`` class, allowing for graceful file
5
- renaming. At present, the filename must be set before defining the url.
6
- * Better handling and/or conditionally raising exceptions.
7
- * Add setup.py.
8
- * Detect duplicate video encoding options, preventing the creation of identical
9
- video class instances. (can cause get() method to raise unnecessary
10
- multiple returned excepion).
11
- * Add functionality for batch URL processing.
12
- * Scrape and inject additional video information into metadata.
 
 
 
 
 
 
 
 
 
 
 
 
 
youtube/youtube.py → pytube/__init__.py RENAMED
@@ -9,36 +9,54 @@ YT_BASE_URL = 'http://www.youtube.com/get_video_info'
9
 
10
  # YouTube media encoding options.
11
  YT_ENCODING = {
12
- 5: (5, "flv", "224p"),
13
- 6: (6, "flv", "270p"),
14
- 13: (13, "3gp", "N/A"),
15
- 17: (17, "3gp", "144p"),
16
- 18: (18, "mp4", "360p"),
17
- 22: (22, "mp4", "720p"),
18
- 34: (34, "flv", "360p"),
19
- 35: (35, "flv", "480p"),
20
- 36: (36, "3gp", "240p"),
21
- 37: (37, "mp4", "1080p"),
22
- 38: (38, "mp4", "3072p"),
23
- 43: (43, "webm", "360p"),
24
- 44: (44, "webm", "480p"),
25
- 45: (45, "webm", "720p"),
26
- 46: (46, "webm", "1080p"),
27
- 82: (82, "mp4", "360p"),
28
- 83: (83, "mp4", "240p"),
29
- 84: (84, "mp4", "720p"),
30
- 85: (85, "mp4", "520p"),
31
- 100: (100, "webm", "360p"),
32
- 101: (101, "webm", "360p"),
33
- 102: (102, "webm", "720p")
 
 
 
 
 
 
 
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  class Video(object):
38
  """
39
  Class representation of a single instance of a YouTube video.
40
  """
41
- def __init__(self, extension, resolution, url, filename):
42
  """
43
  Define the variables required to declare a new video.
44
 
@@ -48,10 +66,10 @@ class Video(object):
48
  url -- The url of the video. (e.g.: youtube.com/watch?v=..)
49
  filename -- The filename (minus the extention) to save the video.
50
  """
51
- self.extension = extension
52
- self.resolution = resolution
53
  self.url = url
54
  self.filename = filename
 
55
 
56
  def download(self, path=None):
57
  """
@@ -85,7 +103,8 @@ class Video(object):
85
 
86
  def __repr__(self):
87
  """A cleaner representation of the class instance."""
88
- return "<Video: %s - %s>" % (self.extension, self.resolution)
 
89
 
90
  def __cmp__(self, other):
91
  if type(other) == Video:
@@ -93,6 +112,7 @@ class Video(object):
93
  v2 = "%s %s" % (other.extension, other.resolution)
94
  return cmp(v1, v2)
95
 
 
96
  class YouTube(object):
97
  _filename = None
98
  _fmt_values = []
@@ -157,10 +177,13 @@ class YouTube(object):
157
  continue
158
  else:
159
  result.append(v)
160
- if len(result) is 1:
 
 
161
  return result[0]
162
  else:
163
- raise Exception("Multiple videos returned")
 
164
 
165
  def filter(self, extension=None, res=None):
166
  """
@@ -219,10 +242,10 @@ class YouTube(object):
219
  resolutions and formats into a list.
220
  """
221
  querystring = urlencode({
222
- 'asv': 3,
223
- 'el': 'detailpage',
224
- 'hl': 'en_US',
225
- 'video_id': self.video_id
226
  })
227
 
228
  self.title = None
@@ -233,10 +256,11 @@ class YouTube(object):
233
  if response:
234
  content = response.read()
235
  data = parse_qs(content)
236
- if data.get('status', [''])[0] == 'failure':
237
- print('Error downloading video: %s' %
238
- data.get('reason', ['Unknown reason'])[0])
239
- return
 
240
 
241
  #Use my cool traversing method to extract the specific
242
  #attribute from the response body.
@@ -253,12 +277,13 @@ class YouTube(object):
253
  #a single empty element, so we'll skip those here.
254
  continue
255
  try:
256
- fmt, ext, res = self._extract_fmt(video)
257
- filename = "%s.%s" % (self.filename, ext)
258
- except TypeError:
259
  pass
260
  else:
261
- self.videos.append(Video(ext, res, url, filename))
 
262
  self._fmt_values.append(fmt)
263
  self.videos.sort()
264
 
@@ -274,7 +299,12 @@ class YouTube(object):
274
  itag = re.findall('itag=(\d+)', text)
275
  if itag and len(itag) is 1:
276
  itag = int(itag[0])
277
- return YT_ENCODING.get(itag, None)
 
 
 
 
 
278
 
279
  def _extract_url(self, text):
280
  """
 
9
 
10
  # YouTube media encoding options.
11
  YT_ENCODING = {
12
+ #Flash Video
13
+ 5: ["flv", "240p", "Sorenson H.263", "N/A", "0.25", "MP3", "64"],
14
+ 6: ["flv", "270p", "Sorenson H.263", "N/A", "0.8", "MP3", "64"],
15
+ 34: ["flv", "360p", "H.264", "Main", "0.5", "AAC", "128"],
16
+ 35: ["flv", "480p", "H.264", "Main", "0.8-1", "AAC", "128"],
17
+
18
+ #3GP
19
+ 36: ["3gp", "240p", "MPEG-4 Visual", "Simple", "0.17", "AAC", "38"],
20
+ 13: ["3gp", "N/A", "MPEG-4 Visual", "N/A", "0.5", "AAC", "N/A"],
21
+ 17: ["3gp", "144p", "MPEG-4 Visual", "Simple", "0.05", "AAC", "24"],
22
+
23
+ #MPEG-4
24
+ 18: ["mp4", "360p", "H.264", "Baseline", "0.5", "AAC", "96"],
25
+ 22: ["mp4", "720p", "H.264", "High", "2-2.9", "AAC", "192"],
26
+ 37: ["mp4", "1080p", "H.264", "High", "3-4.3", "AAC", "192"],
27
+ 38: ["mp4", "3072p", "H.264", "High", "3.5-5", "AAC", "192"],
28
+ 82: ["mp4", "360p", "H.264", "3D", "0.5", "AAC", "96"],
29
+ 83: ["mp4", "240p", "H.264", "3D", "0.5", "AAC", "96"],
30
+ 84: ["mp4", "720p", "H.264", "3D", "2-2.9", "AAC", "152"],
31
+ 85: ["mp4", "520p", "H.264", "3D", "2-2.9", "AAC", "152"],
32
+
33
+ #WebM
34
+ 43: ["webm", "360p", "VP8", "N/A", "0.5", "Vorbis", "128"],
35
+ 44: ["webm", "480p", "VP8", "N/A", "1", "Vorbis", "128"],
36
+ 45: ["webm", "720p", "VP8", "N/A", "2", "Vorbis", "192"],
37
+ 46: ["webm", "1080p", "VP8", "N/A", "N/A", "Vorbis", "192"],
38
+ 100: ["webm", "360p", "VP8", "3D", "N/A", "Vorbis", "128"],
39
+ 101: ["webm", "360p", "VP8", "3D", "N/A", "Vorbis", "192"],
40
+ 102: ["webm", "720p", "VP8", "3D", "N/A", "Vorbis", "192"]
41
  }
42
 
43
+ YT_ENCODING_KEYS = (
44
+ 'extension', 'resolution', 'video_codec', 'profile', 'video_bitrate',
45
+ 'audio_codec', 'audio_bitrate'
46
+ )
47
+
48
+
49
+ class MultipleObjectsReturned(Exception):
50
+ pass
51
+
52
+ class YouTubeError(Exception):
53
+ pass
54
 
55
  class Video(object):
56
  """
57
  Class representation of a single instance of a YouTube video.
58
  """
59
+ def __init__(self, url, filename, **attributes):
60
  """
61
  Define the variables required to declare a new video.
62
 
 
66
  url -- The url of the video. (e.g.: youtube.com/watch?v=..)
67
  filename -- The filename (minus the extention) to save the video.
68
  """
69
+
 
70
  self.url = url
71
  self.filename = filename
72
+ self.__dict__.update(**attributes)
73
 
74
  def download(self, path=None):
75
  """
 
103
 
104
  def __repr__(self):
105
  """A cleaner representation of the class instance."""
106
+ return "<Video: %s (.%s) - %s>" % (self.video_codec, self.extension,
107
+ self.resolution)
108
 
109
  def __cmp__(self, other):
110
  if type(other) == Video:
 
112
  v2 = "%s %s" % (other.extension, other.resolution)
113
  return cmp(v1, v2)
114
 
115
+
116
  class YouTube(object):
117
  _filename = None
118
  _fmt_values = []
 
177
  continue
178
  else:
179
  result.append(v)
180
+ if not len(result):
181
+ return
182
+ elif len(result) is 1:
183
  return result[0]
184
  else:
185
+ raise MultipleObjectsReturned("get() returned more than one " \
186
+ "object -- it returned %d!" % len(result))
187
 
188
  def filter(self, extension=None, res=None):
189
  """
 
242
  resolutions and formats into a list.
243
  """
244
  querystring = urlencode({
245
+ 'asv': 3,
246
+ 'el': 'detailpage',
247
+ 'hl': 'en_US',
248
+ 'video_id': self.video_id
249
  })
250
 
251
  self.title = None
 
256
  if response:
257
  content = response.read()
258
  data = parse_qs(content)
259
+ if 'errorcode' in data:
260
+ error = data.get('reason', 'An unknown error has occurred')
261
+ if isinstance(error, list):
262
+ error = error.pop()
263
+ raise YouTubeError(error)
264
 
265
  #Use my cool traversing method to extract the specific
266
  #attribute from the response body.
 
277
  #a single empty element, so we'll skip those here.
278
  continue
279
  try:
280
+ fmt, data = self._extract_fmt(video)
281
+ filename = "%s.%s" % (self.filename, data['extension'])
282
+ except TypeError, KeyError:
283
  pass
284
  else:
285
+ v = Video(url, filename, **data)
286
+ self.videos.append(v)
287
  self._fmt_values.append(fmt)
288
  self.videos.sort()
289
 
 
299
  itag = re.findall('itag=(\d+)', text)
300
  if itag and len(itag) is 1:
301
  itag = int(itag[0])
302
+ attr = YT_ENCODING.get(itag, None)
303
+ if not attr:
304
+ return itag, None
305
+ data = {}
306
+ map(lambda k, v: data.update({k: v}), YT_ENCODING_KEYS, attr)
307
+ return itag, data
308
 
309
  def _extract_url(self, text):
310
  """
setup.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from setuptools import setup
3
+
4
+ """
5
+ PyTube
6
+ -------
7
+ """
8
+
9
+ def read(fname):
10
+ return open(os.path.join(os.path.dirname(__file__), fname)).read()
11
+
12
+ setup(
13
+ name = "pytube",
14
+ version = "0.0.4",
15
+ author = "Nick Ficano",
16
+ author_email = "[email protected]",
17
+ description = "A lightwight, dependency-free YouTube Video downloading library",
18
+ license='The MIT License: http://www.opensource.org/licenses/mit-license.php',
19
+ keywords = "youtube downloader",
20
+ url = "https://github.com/NFicano/python-youtube-download",
21
+ download_url="https://github.com/NFicano/python-youtube-download/tarball/master",
22
+ packages=['pytube'],
23
+ long_description=read('README.md'),
24
+ classifiers=[
25
+ "Development Status :: - Beta",
26
+ "Environment :: Console"
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 2.6",
30
+ "Programming Language :: Python :: 2.7",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Topic :: Utilities",
33
+ ],
34
+ )
youtube/__init__.py DELETED
File without changes