Commit 5ca0e80f authored by Fjen Undso's avatar Fjen Undso
Browse files

use youtube-dl and only API for library

parent 422835e7
......@@ -5,137 +5,26 @@ from __future__ import unicode_literals
import re
import string
import unicodedata
from multiprocessing.pool import ThreadPool
from urlparse import parse_qs, urlparse
from mopidy import backend
from mopidy.models import Album, SearchResult, Track
import pafy
from mopidy.models import Album, SearchResult, Track, Artist
import pykka
import requests
import youtube_dl
from mopidy_youtube import logger
yt_api_endpoint = 'https://www.googleapis.com/youtube/v3/'
yt_key = 'AIzaSyAl1Xq9DwdE_KD4AtPaE4EJl3WZe2zCqg4'
yt_key = 'AIzaSyBBnJyPjdmmVemPeeEmcOCND9jZCNHVXnQ'
session = requests.Session()
video_uri_prefix = 'youtube:video'
search_uri = 'youtube:search'
def resolve_track(track, stream=False):
logger.debug("Resolving YouTube for track '%s'", track)
if hasattr(track, 'uri'):
return resolve_url(track.comment, stream)
else:
return resolve_url(track.split('.')[-1], stream)
def safe_url(uri):
valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
safe_uri = unicodedata.normalize(
'NFKD',
unicode(uri)
).encode('ASCII', 'ignore')
return re.sub(
'\s+',
' ',
''.join(c for c in safe_uri if c in valid_chars)
).strip()
def resolve_url(url, stream=False):
try:
video = pafy.new(url)
if not stream:
uri = '%s/%s.%s' % (
video_uri_prefix, safe_url(video.title), video.videoid)
else:
uri = video.getbestaudio()
if not uri: # get video url
uri = video.getbest()
logger.debug('%s - %s %s %s' % (
video.title, uri.bitrate, uri.mediatype, uri.extension))
uri = uri.url
if not uri:
return
except Exception as e:
# Video is private or doesn't exist
logger.info(e.message)
return
images = []
if video.bigthumb is not None:
images.append(video.bigthumb)
if video.bigthumbhd is not None:
images.append(video.bigthumbhd)
track = Track(
name=video.title,
comment=video.videoid,
length=video.length * 1000,
album=Album(
name='YouTube',
images=images
),
uri=uri
)
return track
def search_youtube(q):
query = {
'part': 'id',
'maxResults': 15,
'type': 'video',
'q': q,
'key': yt_key
}
result = session.get(yt_api_endpoint + 'search', params=query)
data = result.json()
resolve_pool = ThreadPool(processes=16)
playlist = [item['id']['videoId'] for item in data['items']]
playlist = resolve_pool.map(resolve_url, playlist)
resolve_pool.close()
return [item for item in playlist if item]
def resolve_playlist(url):
resolve_pool = ThreadPool(processes=16)
logger.info("Resolving YouTube-Playlist '%s'", url)
playlist = []
page = 'first'
while page:
params = {
'playlistId': url,
'maxResults': 50,
'key': yt_key,
'part': 'contentDetails'
}
if page and page != "first":
logger.debug("Get YouTube-Playlist '%s' page %s", url, page)
params['pageToken'] = page
result = session.get(yt_api_endpoint + 'playlistItems', params=params)
data = result.json()
page = data.get('nextPageToken')
for item in data["items"]:
video_id = item['contentDetails']['videoId']
playlist.append(video_id)
playlist = resolve_pool.map(resolve_url, playlist)
resolve_pool.close()
return [item for item in playlist if item]
class YouTubeBackend(pykka.ThreadingActor, backend.Backend):
def __init__(self, config, audio):
super(YouTubeBackend, self).__init__()
......@@ -147,57 +36,129 @@ class YouTubeBackend(pykka.ThreadingActor, backend.Backend):
class YouTubeLibraryProvider(backend.LibraryProvider):
def lookup(self, track):
if 'yt:' in track:
track = track.replace('yt:', '')
if 'youtube.com' in track:
url = urlparse(track)
def _get_duration(self, yttime):
# convert PT1H2M10S to 3730
m = re.search('PT((?P<hours>\d+)H)?' +
'((?P<minutes>\d+)M)?' +
'((?P<seconds>\d+)S)?',
yttime)
return (int(m.group('hours') or 0) * 3600 +
int(m.group('minutes') or 0) * 60 +
int(m.group('seconds') or 0)) * 1000
def _get_videos(self, ids):
"""Get list of youtube ids and return tracks"""
logger.info("Get videos for '%s'", str(ids))
query = {
'part': 'id,snippet,contentDetails',
'fields': 'items(id,snippet(title,channelTitle,thumbnails(high(url))),contentDetails(duration))',
'id' : ','.join(ids),
'key': yt_key
}
results = session.get(yt_api_endpoint + 'videos', params=query).json()['items']
tracks = []
for result in results:
track = Track(
artists=[Artist(name='YouTube')],
name=result['snippet']['title'],
length=self._get_duration(result['contentDetails']['duration']),
album=Album(
name=result['id'],
images=[result['snippet']['thumbnails']['high']['url']]
),
uri='yt:' + result['id']
)
tracks.append(track)
return tracks
def _get_playlist(self, id, maxresults=30):
logger.info("Get Playlist for '%s'", str(id))
query = {
'part': 'id,snippet',
'fields': 'nextPageToken,items(snippet(resourceId(videoId)))',
'maxResults': maxresults,
'playlistId': id,
'key': yt_key,
'pageToken': '',
}
results = session.get(yt_api_endpoint + 'playlistItems', params=query).json()['items']
videoids = [x['snippet']['resourceId']['videoId'] for x in results]
return self._get_videos(videoids)
def _get_search(self, query, maxresults=20):
query = {
'part': 'id',
'fields': 'items(id)',
'maxResults': maxresults,
'type': 'video',
'q': query,
'key': yt_key
}
results = session.get(yt_api_endpoint + 'search', params=query).json()['items']
videoids = [x['id']['videoId'] for x in results]
return self._get_videos(videoids)
def lookup(self, uri):
logger.info("Looking up YouTube for '%s'", uri)
for scheme in self.backend.uri_schemes:
if uri.startswith(scheme):
uri = uri[len(scheme)+1:]
if 'youtube.com' in uri:
url = urlparse(uri)
req = parse_qs(url.query)
if 'list' in req:
return resolve_playlist(req.get('list')[0])
else:
return [item for item in [resolve_url(track)] if item]
return self._get_playlist(req.get('list')[0])
if 'v' in req:
return self._get_videos([req.get('v')[0]])
else:
return [item for item in [resolve_track(track)] if item]
return self._get_videos([uri])
def search(self, query=None, uris=None, exact=False):
# TODO Support exact search
logger.info("Searching YouTube for query '%s'", query)
if not query:
return
if 'uri' in query:
search_query = ''.join(query['uri'])
url = urlparse(search_query)
if 'youtube.com' in url.netloc:
req = parse_qs(url.query)
if 'list' in req:
return SearchResult(
uri=search_uri,
tracks=resolve_playlist(req.get('list')[0])
)
else:
logger.info(
"Resolving YouTube for track '%s'", search_query)
return SearchResult(
uri=search_uri,
tracks=[t for t in [resolve_url(search_query)] if t]
)
else:
if 'any' in query:
search_query = ' '.join(query.values()[0])
logger.info("Searching YouTube for query '%s'", search_query)
return SearchResult(
uri=search_uri,
tracks=search_youtube(search_query)
tracks=self._get_search(search_query)
)
return None
class YouTubePlaybackProvider(backend.PlaybackProvider):
def translate_uri(self, uri):
track = resolve_track(uri, True)
if track is not None:
return track.uri
else:
logger.info("Playing YouTube id: %s" % uri)
for scheme in self.backend.uri_schemes:
if uri.startswith(scheme):
uri = uri[len(scheme)+1:]
track = self._resolve_url(uri)
return track
def _resolve_url(self, url):
ytOpts = {
'format': 'bestaudio/best'
}
try:
with youtube_dl.YoutubeDL(ytOpts) as ydl:
ytUri = ydl.extract_info(
url=url,
download=False
)
except:
return None
if 'entries' in ytUri: # if playlist
trackList = ytUri['entries']
else:
trackList = [ytUri]
return trackList[0]['url']
......@@ -26,7 +26,7 @@ setup(
install_requires=[
'setuptools',
'requests >= 2.2.1',
'pafy >= 0.3.35',
'youtube_dl >= 2016.01.01',
'Mopidy >= 1.0',
'Pykka >= 1.1',
],
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment