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 = "nficano@gmail.com",
|
| 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
|