1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2011 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # gpodder.youtube - YouTube and related magic
20 # Justin Forest <justin.forest@gmail.com> 2008-10-13
26 from gpodder
import util
27 from gpodder
.liblogger
import log
30 import simplejson
as json
37 # See http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
38 # Currently missing: the WebM 480p and 720 formats; 3GP profile
40 (37, '37/1920x1080/9/0/115', '1920x1080 (HD)'),
41 (22, '22/1280x720/9/0/115', '1280x720 (HD)'),
42 (35, '35/854x480/9/0/115', '854x480'),
43 (34, '34/640x360/9/0/115', '640x360'),
44 (18, '18/640x360/9/0/115', '640x360 (iPod)'),
45 (18, '18/480x360/9/0/115', '480x360 (iPod)'),
46 (5, '5/320x240/7/0/0', '320x240 (FLV)'),
49 class YouTubeError(Exception): pass
51 def get_real_download_url(url
, preferred_fmt_id
=None):
52 # Default fmt_id when none preferred
53 if preferred_fmt_id
is None:
56 # For Maemo 5, we force fmt_id 5 for performance reasons
57 if gpodder
.ui
.fremantle
and not gpodder
.ui
.fermintle
:
60 vid
= get_youtube_id(url
)
63 url
= 'http://www.youtube.com/watch?v=' + vid
66 req
= util
.http_request(url
, method
='GET')
67 if 'location' in req
.msg
:
68 url
= req
.msg
['location']
72 # Try to find the best video format available for this video
73 # (http://forum.videohelp.com/topic336882-1800.html#1912972)
75 r4
= re
.search('.*"fmt_url_map"\:\s+"([^"]+)".*', page
)
77 fmt_url_map
= r4
.group(1)
78 for fmt_url_encoded
in fmt_url_map
.split(','):
79 fmt_url
= urllib
.unquote(fmt_url_encoded
)
80 fmt_url
= fmt_url
.replace('\\/', '/').replace('\u0026', '&')
81 fmt_id
, url
= fmt_url
.split('|', 2)
82 yield int(fmt_id
), url
84 fmt_id_url_map
= sorted(find_urls(page
), reverse
=True)
85 # Default to the highest fmt_id if we don't find a match below
87 default_fmt_id
, default_url
= fmt_id_url_map
[0]
89 raise YouTubeError('fmt_url_map not found for video ID "%s"' % vid
)
91 formats_available
= set(fmt_id
for fmt_id
, url
in fmt_id_url_map
)
92 fmt_id_url_map
= dict(fmt_id_url_map
)
94 if gpodder
.ui
.fremantle
:
95 # This provides good quality video, seems to be always available
96 # and is playable fluently in Media Player
97 if preferred_fmt_id
== 5:
102 # As a fallback, use fmt_id 18 (seems to be always available)
105 # This will be set to True if the search below has already "seen"
106 # our preferred format, but has not yet found a suitable available
107 # format for the given video.
108 seen_preferred
= False
110 for id, wanted
, description
in supported_formats
:
111 # If we see our preferred format, accept formats below
112 if id == preferred_fmt_id
:
113 seen_preferred
= True
115 # If the format is available and preferred (or lower),
116 # use the given format for our fmt_id
117 if id in formats_available
and seen_preferred
:
118 log('Found available YouTube format: %s (fmt_id=%d)', \
123 url
= fmt_id_url_map
.get(fmt_id
, None)
129 def get_youtube_id(url
):
130 r
= re
.compile('http://(?:[a-z]+\.)?youtube\.com/v/(.*)\.swf', re
.IGNORECASE
).match(url
)
134 r
= re
.compile('http://(?:[a-z]+\.)?youtube\.com/watch\?v=([^&]*)', re
.IGNORECASE
).match(url
)
140 def is_video_link(url
):
141 return (get_youtube_id(url
) is not None)
143 def get_real_channel_url(url
):
144 r
= re
.compile('http://(?:[a-z]+\.)?youtube\.com/user/([a-z0-9]+)', re
.IGNORECASE
)
148 next
= 'http://www.youtube.com/rss/user/'+ m
.group(1) +'/videos.rss'
149 log('YouTube link resolved: %s => %s', url
, next
)
152 r
= re
.compile('http://(?:[a-z]+\.)?youtube\.com/profile?user=([a-z0-9]+)', re
.IGNORECASE
)
156 next
= 'http://www.youtube.com/rss/user/'+ m
.group(1) +'/videos.rss'
157 log('YouTube link resolved: %s => %s', url
, next
)
162 def get_real_cover(url
):
163 r
= re
.compile('http://www\.youtube\.com/rss/user/([^/]+)/videos\.rss', \
168 username
= m
.group(1)
169 api_url
= 'http://gdata.youtube.com/feeds/api/users/%s?v=2' % username
170 data
= util
.urlopen(api_url
).read()
171 match
= re
.search('<media:thumbnail url=[\'"]([^\'"]+)[\'"]/>', data
)
172 if match
is not None:
173 log('YouTube userpic for %s is: %s', url
, match
.group(1))
174 return match
.group(1)
178 def find_youtube_channels(string
):
179 url
= 'http://gdata.youtube.com/feeds/api/videos?alt=json&q=%s' % urllib
.quote(string
, '')
180 data
= json
.load(util
.urlopen(url
))
182 class FakeImporter(object):
186 result
= FakeImporter()
189 for entry
in data
['feed']['entry']:
190 user
= entry
['author'][0]['name']['$t']
191 title
= entry
['title']['$t']
192 url
= 'http://www.youtube.com/rss/user/%s/videos.rss' % user
193 if user
not in seen_users
:
194 result
.items
.append({