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 {
|
||||
max-width: 60em;
|
||||
margin: 3em auto 1em auto;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
#video-search input[type=text] {
|
||||
|
@ -11,3 +11,64 @@ form#video-ctrl {
|
|||
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();
|
||||
});
|
||||
|
||||
$volumeCtrl.on('mouseup', 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) {
|
||||
$volumeCtrl.on('mouseup touchend', function(event) {
|
||||
execute(
|
||||
{
|
||||
type: 'request',
|
||||
|
|
|
@ -1,41 +1,66 @@
|
|||
$(document).ready(function() {
|
||||
var $container = $('#video-container'),
|
||||
$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() {
|
||||
$searchForm.on('submit', function(event) {
|
||||
var formData = $(this).serializeArray().reduce(function(obj, item) {
|
||||
var value = item.value.trim();
|
||||
if (value.length > 0) {
|
||||
obj[item.name] = item.value;
|
||||
}
|
||||
var $input = $(this).find('input[name=video-search-text]');
|
||||
var resource = $input.val();
|
||||
var request = {}
|
||||
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://'))) {
|
||||
var videos = [{ url: resource }];
|
||||
updateVideoResults(videos);
|
||||
|
||||
request = {
|
||||
type: 'request',
|
||||
action: 'video.omxplayer.stop',
|
||||
},
|
||||
action: 'video.omxplayer.play',
|
||||
args: { resource: resource }
|
||||
};
|
||||
} else {
|
||||
request = {
|
||||
type: 'request',
|
||||
action: 'video.omxplayer.youtube_search',
|
||||
args: { query: resource }
|
||||
};
|
||||
|
||||
function() {
|
||||
execute(
|
||||
{
|
||||
type: 'request',
|
||||
action: 'video.omxplayer.play',
|
||||
args: formData,
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
onSuccess = function(response) {
|
||||
var videos = response.response.output;
|
||||
updateVideoResults(videos);
|
||||
};
|
||||
}
|
||||
|
||||
execute(request, onSuccess, onError, onComplete)
|
||||
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 $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() {
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
<div class="row" id="video-container">
|
||||
<form action="#" id="video-search">
|
||||
<div class="row">
|
||||
<label for="resource">
|
||||
Supported formats: <tt>file://[path]</tt>, <tt>https://www.youtube.com/?v=[video_id]</tt>
|
||||
<label for="video-search-text">
|
||||
Supported formats: <tt>file://[path]</tt>, <tt>https://www.youtube.com/?v=[video_id]</tt>, or free text search
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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 class="one column">
|
||||
<button type="submit">
|
||||
|
@ -22,6 +22,14 @@
|
|||
</form>
|
||||
|
||||
<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="ten columns offset-by-one">
|
||||
<button data-action="previous">
|
||||
|
@ -52,6 +60,19 @@
|
|||
<i class="fa fa-step-forward"></i>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<div class="row" id="video-results-container">
|
||||
<div id="video-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -30,6 +30,15 @@ class VideoOmxplayerPlugin(Plugin):
|
|||
|
||||
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:
|
||||
self.player = OMXPlayer(resource, args=self.args)
|
||||
self._init_player_handlers()
|
||||
|
@ -145,29 +154,40 @@ class VideoOmxplayerPlugin(Plugin):
|
|||
'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):
|
||||
if not self.player:
|
||||
return
|
||||
|
||||
self.player.playEvent += lambda _: \
|
||||
self.bus.post(VideoPlayEvent(video=self.player.get_source()))
|
||||
|
||||
self.player.pauseEvent += lambda _: \
|
||||
self.bus.post(VideoPauseEvent(video=self.player.get_source()))
|
||||
|
||||
self.player.stopEvent += lambda _: \
|
||||
self.bus.post(VideoStopEvent())
|
||||
self.player.playEvent += self.on_play()
|
||||
self.player.pauseEvent += self.on_pause()
|
||||
self.player.stopEvent += self.on_stop()
|
||||
|
||||
def youtube_search_and_play(self, query):
|
||||
self.videos_queue = self.youtube_search(query)
|
||||
self.videos_queue = self.youtube_search(query).output
|
||||
ret = None
|
||||
|
||||
while self.videos_queue:
|
||||
url = self.videos_queue.pop(0)
|
||||
logging.info('Playing {}'.format(url))
|
||||
video = self.videos_queue.pop(0)
|
||||
logging.info('Playing "{}" from [{}]'.format(video['url'], video['title']))
|
||||
|
||||
try:
|
||||
ret = self.play(url)
|
||||
ret = self.play(video['url'])
|
||||
break
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
@ -186,12 +206,15 @@ class VideoOmxplayerPlugin(Plugin):
|
|||
for vid in soup.findAll(attrs={'class':'yt-uix-tile-link'}):
|
||||
m = re.match('(/watch\?v=[^&]+)', vid['href'])
|
||||
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 "{}"'
|
||||
.format(len(results), query))
|
||||
|
||||
return results
|
||||
return Response(output=results)
|
||||
|
||||
|
||||
@classmethod
|
||||
|
|
Loading…
Reference in a new issue