Implemented YouTube videos search and controls web FE

This commit is contained in:
Fabio Manganiello 2018-04-24 14:36:05 +02:00
parent 4d45284131
commit dd254b65cb
5 changed files with 221 additions and 58 deletions

View file

@ -1,6 +1,6 @@
#video-search { #video-search {
max-width: 60em; max-width: 60em;
margin: 3em auto 1em auto; margin: 1em auto;
} }
#video-search input[type=text] { #video-search input[type=text] {
@ -11,3 +11,64 @@ form#video-ctrl {
text-align: center; text-align: center;
} }
#video-seeker-container {
margin-top: 0.5em;
margin-bottom: 1em;
}
#video-volume-ctrl-container {
margin-top: 1em;
}
#video-results {
padding: 1.5rem 1.5rem 0 .5rem;
background: #f8f8f8;
}
.video-result {
padding: 5px;
letter-spacing: .1rem;
line-height: 3.3rem;
cursor: pointer;
}
.video-result.selected {
background-color: #c8ffd0 !important;
}
.video-result:hover {
background-color: #daf8e2 !important;
}
.video-result:nth-child(odd) {
background-color: #f2f2f2;
}
.video-result.active {
height: 4rem;
padding-top: 1.5rem;
font-size: 1.7rem;
border-radius: 10px;
animation: active-track 5s;
-moz-animation: active-track 5s infinite;
-webkit-animation: active-track 5s infinite;
}
@keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
@-moz-keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
@-webkit-keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}

View file

@ -521,26 +521,11 @@ $(document).ready(function() {
); );
}); });
$volumeCtrl.on('mousedown', function(event) { $volumeCtrl.on('mousedown touchstart', function(event) {
prevVolume = $(this).val(); prevVolume = $(this).val();
}); });
$volumeCtrl.on('mouseup', function(event) { $volumeCtrl.on('mouseup touchend', function(event) {
execute(
{
type: 'request',
action: 'music.mpd.setvol',
args: { vol: $(this).val() }
},
onSuccess=undefined,
onError = function() {
$volumeCtrl.val(prevVolume);
}
);
});
$volumeCtrl.on('mouseup', function(event) {
execute( execute(
{ {
type: 'request', type: 'request',

View file

@ -1,41 +1,66 @@
$(document).ready(function() { $(document).ready(function() {
var $container = $('#video-container'), var $container = $('#video-container'),
$searchForm = $('#video-search'), $searchForm = $('#video-search'),
$ctrlForm = $('#video-ctrl'); $videoResults = $('#video-results'),
$volumeCtrl = $('#video-volume-ctrl'),
$ctrlForm = $('#video-ctrl'),
prevVolume = undefined;
var updateVideoResults = function(videos) {
$videoResults.html('');
for (var video of videos) {
var $videoResult = $('<div></div>')
.addClass('video-result')
.attr('data-url', video['url'])
.html('title' in video ? video['title'] : video['url']);
$videoResult.appendTo($videoResults);
}
};
var initBindings = function() { var initBindings = function() {
$searchForm.on('submit', function(event) { $searchForm.on('submit', function(event) {
var formData = $(this).serializeArray().reduce(function(obj, item) { var $input = $(this).find('input[name=video-search-text]');
var value = item.value.trim(); var resource = $input.val();
if (value.length > 0) { var request = {}
obj[item.name] = item.value; var onSuccess = function() {};
} var onError = function() {};
var onComplete = function() {
$input.prop('disabled', false);
};
return obj; $input.prop('disabled', true);
}, {}); $videoResults.text('Searching...');
execute( if (resource.match(new RegExp('^https?://')) ||
{ resource.match(new RegExp('^file://'))) {
type: 'request', var videos = [{ url: resource }];
action: 'video.omxplayer.stop', updateVideoResults(videos);
},
function() { request = {
execute(
{
type: 'request', type: 'request',
action: 'video.omxplayer.play', action: 'video.omxplayer.play',
args: formData, args: { resource: resource }
} };
) } else {
} request = {
); type: 'request',
action: 'video.omxplayer.youtube_search',
args: { query: resource }
};
onSuccess = function(response) {
var videos = response.response.output;
updateVideoResults(videos);
};
}
execute(request, onSuccess, onError, onComplete)
return false; return false;
}); });
$ctrlForm.on('submit', function() { return false; }); $ctrlForm.on('submit', function() { return false; });
$ctrlForm.find('button[data-action]').on('click', function(evt) { $ctrlForm.find('button[data-action]').on('click touch', function(evt) {
var action = $(this).data('action'); var action = $(this).data('action');
var $btn = $(this); var $btn = $(this);
@ -46,6 +71,54 @@ $(document).ready(function() {
} }
); );
}); });
$volumeCtrl.on('mousedown touchstart', function(event) {
prevVolume = $(this).val();
});
$volumeCtrl.on('mouseup touchend', function(event) {
execute(
{
type: 'request',
action: 'video.omxplayer.set_volume',
args: { volume: $(this).val() }
},
onSuccess=undefined,
onError = function() {
$volumeCtrl.val(prevVolume);
}
);
});
$videoResults.on('click touch', '.video-result', function(evt) {
var results = $videoResults.html();
var $item = $(this);
if (!$item.hasClass('selected')) {
$item.siblings().removeClass('selected');
$item.addClass('selected');
return false;
}
$videoResults.text('Loading video...');
execute(
{
type: 'request',
action: 'video.omxplayer.play',
args: { resource: $item.data('url') },
},
function() {
$videoResults.html(results);
$item.siblings().removeClass('active');
$item.addClass('active');
},
function() {
$videoResults.html(results);
},
);
});
}; };
var init = function() { var init = function() {

View file

@ -4,14 +4,14 @@
<div class="row" id="video-container"> <div class="row" id="video-container">
<form action="#" id="video-search"> <form action="#" id="video-search">
<div class="row"> <div class="row">
<label for="resource"> <label for="video-search-text">
Supported formats: <tt>file://[path]</tt>, <tt>https://www.youtube.com/?v=[video_id]</tt> Supported formats: <tt>file://[path]</tt>, <tt>https://www.youtube.com/?v=[video_id]</tt>, or free text search
</label> </label>
</div> </div>
<div class="row"> <div class="row">
<div class="eleven columns"> <div class="eleven columns">
<input type="text" name="resource" placeholder="Video URL"> <input type="text" name="video-search-text" placeholder="Search query or video URL">
</div> </div>
<div class="one column"> <div class="one column">
<button type="submit"> <button type="submit">
@ -22,6 +22,14 @@
</form> </form>
<form action="#" id="video-ctrl"> <form action="#" id="video-ctrl">
<!-- <div class="row"> -->
<!-- <div class="eight columns offset-by-two slider-container" id="video-seeker-container"> -->
<!-- <span class="seek-time" id="video-elapsed">-:--</span>&nbsp; -->
<!-- <input type="range" min="0" id="video-seeker" disabled="disabled" class="slider" style="width:75%"> -->
<!-- &nbsp;<span class="seek-time" id="video-length">-:--</span> -->
<!-- </div> -->
<!-- </div> -->
<div class="row"> <div class="row">
<div class="ten columns offset-by-one"> <div class="ten columns offset-by-one">
<button data-action="previous"> <button data-action="previous">
@ -52,6 +60,19 @@
<i class="fa fa-step-forward"></i> <i class="fa fa-step-forward"></i>
</button> </button>
</div> </div>
</form> </div>
<div class="row">
<div class="eight columns offset-by-two slider-container" id="video-volume-ctrl-container">
<i class="fa fa-volume-down"></i> &nbsp;
<input type="range" min="0" max="100" value="100" id="video-volume-ctrl" class="slider" style="width:80%">
&nbsp; <i class="fa fa-volume-up"></i>
</div>
</div>
</form>
<div class="row" id="video-results-container">
<div id="video-results"></div>
</div>
</div> </div>

View file

@ -30,6 +30,15 @@ class VideoOmxplayerPlugin(Plugin):
logging.info('Playing {}'.format(resource)) logging.info('Playing {}'.format(resource))
if self.player:
try:
self.player.stop()
self.player = None
except Exception as e:
logging.exception(e)
logging.warning('Unable to stop a previously running instance ' +
'of OMXPlayer, trying to play anyway')
try: try:
self.player = OMXPlayer(resource, args=self.args) self.player = OMXPlayer(resource, args=self.args)
self._init_player_handlers() self._init_player_handlers()
@ -145,29 +154,40 @@ class VideoOmxplayerPlugin(Plugin):
'state': PlayerState.STOP.value 'state': PlayerState.STOP.value
})) }))
def on_play(self):
def _f(player):
self.bus.post(VideoPlayEvent(video=self.player.get_source()))
return _f
def on_pause(self):
def _f(player):
self.bus.post(VideoPauseEvent(video=self.player.get_source()))
return _f
def on_stop(self):
def _f(player):
self.bus.post(VideoStopEvent())
return _f
def _init_player_handlers(self): def _init_player_handlers(self):
if not self.player: if not self.player:
return return
self.player.playEvent += lambda _: \ self.player.playEvent += self.on_play()
self.bus.post(VideoPlayEvent(video=self.player.get_source())) self.player.pauseEvent += self.on_pause()
self.player.stopEvent += self.on_stop()
self.player.pauseEvent += lambda _: \
self.bus.post(VideoPauseEvent(video=self.player.get_source()))
self.player.stopEvent += lambda _: \
self.bus.post(VideoStopEvent())
def youtube_search_and_play(self, query): def youtube_search_and_play(self, query):
self.videos_queue = self.youtube_search(query) self.videos_queue = self.youtube_search(query).output
ret = None ret = None
while self.videos_queue: while self.videos_queue:
url = self.videos_queue.pop(0) video = self.videos_queue.pop(0)
logging.info('Playing {}'.format(url)) logging.info('Playing "{}" from [{}]'.format(video['url'], video['title']))
try: try:
ret = self.play(url) ret = self.play(video['url'])
break break
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
@ -186,12 +206,15 @@ class VideoOmxplayerPlugin(Plugin):
for vid in soup.findAll(attrs={'class':'yt-uix-tile-link'}): for vid in soup.findAll(attrs={'class':'yt-uix-tile-link'}):
m = re.match('(/watch\?v=[^&]+)', vid['href']) m = re.match('(/watch\?v=[^&]+)', vid['href'])
if m: if m:
results.append('https://www.youtube.com' + m.group(1)) results.append({
'url': 'https://www.youtube.com' + m.group(1),
'title': vid['title'],
})
logging.info('{} YouTube video results for the search query "{}"' logging.info('{} YouTube video results for the search query "{}"'
.format(len(results), query)) .format(len(results), query))
return results return Response(output=results)
@classmethod @classmethod