Added better exception handling, fixed a ton of bugs, added setup.py.
Browse files- README.md +43 -38
- TODO +0 -12
- youtube/youtube.py → pytube/__init__.py +71 -41
- setup.py +34 -0
- 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
|
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 |
-
|
54 |
-
|
55 |
-
#[<Video: 3gp - 144p>,
|
56 |
-
# <Video: 3gp -
|
57 |
-
# <Video:
|
58 |
-
# <Video:
|
59 |
-
# <Video: flv -
|
60 |
-
# <Video:
|
61 |
-
# <Video:
|
62 |
-
# <Video:
|
63 |
-
# <Video:
|
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 |
-
|
88 |
|
89 |
-
#
|
90 |
-
# <Video: flv -
|
91 |
-
# <Video: flv -
|
92 |
-
# <Video: flv - 360p>,
|
93 |
-
# <Video: flv - 480p>,
|
94 |
-
# <Video: flv - 480p>]
|
95 |
|
96 |
# and by resolution
|
97 |
-
|
98 |
|
99 |
-
#
|
100 |
-
|
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 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
}
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
37 |
class Video(object):
|
38 |
"""
|
39 |
Class representation of a single instance of a YouTube video.
|
40 |
"""
|
41 |
-
def __init__(self,
|
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 |
-
|
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.
|
|
|
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)
|
|
|
|
|
161 |
return result[0]
|
162 |
else:
|
163 |
-
raise
|
|
|
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 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
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
|
237 |
-
|
238 |
-
|
239 |
-
|
|
|
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,
|
257 |
-
filename = "%s.%s" % (self.filename,
|
258 |
-
except TypeError:
|
259 |
pass
|
260 |
else:
|
261 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
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
|