forked from platypush/platypush
Implemented YouTube videos search and controls web FE
This commit is contained in:
parent
4d45284131
commit
dd254b65cb
5 changed files with 221 additions and 58 deletions
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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> -->
|
||||||
|
<!-- <input type="range" min="0" id="video-seeker" disabled="disabled" class="slider" style="width:75%"> -->
|
||||||
|
<!-- <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>
|
||||||
|
</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>
|
||||||
|
<input type="range" min="0" max="100" value="100" id="video-volume-ctrl" class="slider" style="width:80%">
|
||||||
|
<i class="fa fa-volume-up"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="row" id="video-results-container">
|
||||||
|
<div id="video-results"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue