Added YouTube support to new media webplugin
This commit is contained in:
parent
cf23e2fc72
commit
e55735f409
16 changed files with 256 additions and 74 deletions
|
@ -1,4 +1,10 @@
|
||||||
.media-plugin {
|
.media-plugin {
|
||||||
|
#media-info {
|
||||||
|
.modal {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-container {
|
.info-container {
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
MediaHandlers.base = Vue.extend({
|
||||||
|
props: {
|
||||||
|
bus: { type: Object },
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dropdownItems: function() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Play',
|
||||||
|
icon: 'play',
|
||||||
|
action: this.play,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'View info',
|
||||||
|
icon: 'info',
|
||||||
|
action: this.info,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
matchesUrl: function(url) {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMetadata: async function(item, onlyBase=false) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
play: function(item) {
|
||||||
|
this.bus.$emit('play', item);
|
||||||
|
},
|
||||||
|
|
||||||
|
info: async function(item) {
|
||||||
|
this.bus.$emit('info-loading');
|
||||||
|
this.bus.$emit('info', {...item, ...(await this.getMetadata(item))});
|
||||||
|
},
|
||||||
|
|
||||||
|
infoLoad: function(url) {
|
||||||
|
if (!this.matchesUrl(url))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.info(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
searchSubtitles: function(item) {
|
||||||
|
this.bus.$emit('search-subs', item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created: function() {
|
||||||
|
const self = this;
|
||||||
|
setTimeout(() => {
|
||||||
|
self.infoLoadWatch = self.bus.$on('info-load', this.infoLoad);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
MediaHandlers.file = Vue.extend({
|
MediaHandlers.file = MediaHandlers.base.extend({
|
||||||
props: {
|
props: {
|
||||||
bus: { type: Object },
|
|
||||||
iconClass: {
|
iconClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'fa fa-hdd',
|
default: 'fa fa-hdd',
|
||||||
|
@ -64,10 +63,6 @@ MediaHandlers.file = Vue.extend({
|
||||||
return item;
|
return item;
|
||||||
},
|
},
|
||||||
|
|
||||||
play: function(item) {
|
|
||||||
this.bus.$emit('play', item);
|
|
||||||
},
|
|
||||||
|
|
||||||
download: async function(item) {
|
download: async function(item) {
|
||||||
this.bus.$on('streaming-started', (media) => {
|
this.bus.$on('streaming-started', (media) => {
|
||||||
if (media.resource === item.url) {
|
if (media.resource === item.url) {
|
||||||
|
@ -78,41 +73,35 @@ MediaHandlers.file = Vue.extend({
|
||||||
|
|
||||||
this.bus.$emit('start-streaming', item.url);
|
this.bus.$emit('start-streaming', item.url);
|
||||||
},
|
},
|
||||||
|
|
||||||
info: async function(item) {
|
|
||||||
this.bus.$emit('info-loading');
|
|
||||||
this.bus.$emit('info', (await this.getMetadata(item)));
|
|
||||||
},
|
|
||||||
|
|
||||||
infoLoad: function(url) {
|
|
||||||
if (!this.matchesUrl(url))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.info(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
searchSubtitles: function(item) {
|
|
||||||
this.bus.$emit('search-subs', item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created: function() {
|
|
||||||
const self = this;
|
|
||||||
setTimeout(() => {
|
|
||||||
self.infoLoadWatch = self.bus.$on('info-load', this.infoLoad);
|
|
||||||
}, 1000);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
MediaHandlers.generic = MediaHandlers.file.extend({
|
MediaHandlers.generic = MediaHandlers.file.extend({
|
||||||
props: {
|
props: {
|
||||||
bus: { type: Object },
|
|
||||||
iconClass: {
|
iconClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'fa fa-globe',
|
default: 'fa fa-globe',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dropdownItems: function() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Play',
|
||||||
|
icon: 'play',
|
||||||
|
action: this.play,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'View info',
|
||||||
|
icon: 'info',
|
||||||
|
action: this.info,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getMetadata: async function(url) {
|
getMetadata: async function(url) {
|
||||||
return {
|
return {
|
||||||
|
@ -120,10 +109,6 @@ MediaHandlers.generic = MediaHandlers.file.extend({
|
||||||
title: url,
|
title: url,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
info: async function(item) {
|
|
||||||
this.bus.$emit('info', (await this.getMetadata(item)));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
MediaHandlers.youtube = Vue.extend({
|
MediaHandlers.youtube = MediaHandlers.base.extend({
|
||||||
props: {
|
props: {
|
||||||
bus: { type: Object },
|
|
||||||
iconClass: {
|
iconClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'fab fa-youtube',
|
default: 'fab fa-youtube',
|
||||||
|
@ -19,7 +18,13 @@ MediaHandlers.youtube = Vue.extend({
|
||||||
{
|
{
|
||||||
text: 'Download (on server)',
|
text: 'Download (on server)',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
action: this.download,
|
action: this.downloadServer,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Download (on client)',
|
||||||
|
icon: 'download',
|
||||||
|
action: this.downloadClient,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -37,17 +42,55 @@ MediaHandlers.youtube = Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getMetadata: function(url) {
|
getMetadata: function(url) {
|
||||||
// TODO
|
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
play: function(item) {
|
_getRawUrl: async function(url) {
|
||||||
|
if (url.indexOf('.googlevideo.com') < 0) {
|
||||||
|
url = await request('media.get_youtube_url', {url: url});
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
download: function(item) {
|
play: async function(item) {
|
||||||
|
if (typeof item === 'string')
|
||||||
|
item = {url: item};
|
||||||
|
|
||||||
|
let url = await this._getRawUrl(item.url);
|
||||||
|
this.bus.$emit('play', {...item, url:url});
|
||||||
},
|
},
|
||||||
|
|
||||||
info: function(item) {
|
downloadServer: async function(item) {
|
||||||
|
createNotification({
|
||||||
|
text: 'Downloading video',
|
||||||
|
image: {
|
||||||
|
icon: 'download',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let url = await this._getRawUrl(item.url);
|
||||||
|
let args = {
|
||||||
|
url: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.title) {
|
||||||
|
args.filename = item.title + '.webm';
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = await request('media.download', args);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: 'Video downloaded to ' + path,
|
||||||
|
image: {
|
||||||
|
icon: 'check',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadClient: async function(item) {
|
||||||
|
let url = await this._getRawUrl(item.url);
|
||||||
|
window.open(url, '_blank');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -110,6 +110,9 @@ Vue.component('media', {
|
||||||
|
|
||||||
let status = await this.selectedDevice.play(item.url, item.subtitles);
|
let status = await this.selectedDevice.play(item.url, item.subtitles);
|
||||||
|
|
||||||
|
if (item.title)
|
||||||
|
status.title = item.title;
|
||||||
|
|
||||||
this.subsModal.visible = false;
|
this.subsModal.visible = false;
|
||||||
this.onStatusUpdate({
|
this.onStatusUpdate({
|
||||||
device: this.selectedDevice,
|
device: this.selectedDevice,
|
||||||
|
@ -207,6 +210,10 @@ Vue.component('media', {
|
||||||
const status = event.status;
|
const status = event.status;
|
||||||
this.syncPosition(status);
|
this.syncPosition(status);
|
||||||
|
|
||||||
|
if (status.state !== 'stop' && this.status[dev.type] && this.status[dev.type][dev.name]) {
|
||||||
|
status.title = status.title || this.status[dev.type][dev.name].title;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.status[dev.type])
|
if (!this.status[dev.type])
|
||||||
Vue.set(this.status, dev.type, {});
|
Vue.set(this.status, dev.type, {});
|
||||||
Vue.set(this.status[dev.type], dev.name, status);
|
Vue.set(this.status[dev.type], dev.name, status);
|
||||||
|
@ -224,10 +231,20 @@ Vue.component('media', {
|
||||||
if (event.plugin.startsWith('media.'))
|
if (event.plugin.startsWith('media.'))
|
||||||
event.plugin = event.plugin.substr(6);
|
event.plugin = event.plugin.substr(6);
|
||||||
|
|
||||||
if (this.status[event.player] && this.status[event.player][event.plugin])
|
var type, player;
|
||||||
Vue.set(this.status[event.player], event.plugin, status);
|
if (this.status[event.player] && this.status[event.player][event.plugin]) {
|
||||||
else if (this.status[event.plugin] && this.status[event.plugin][event.player])
|
type = event.player;
|
||||||
Vue.set(this.status[event.plugin], event.player, status);
|
player = event.plugin;
|
||||||
|
} else if (this.status[event.plugin] && this.status[event.plugin][event.player]) {
|
||||||
|
type = event.plugin;
|
||||||
|
player = event.player;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.state !== 'stop') {
|
||||||
|
status.title = status.title || this.status[type][player].title;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.set(this.status[type], player, status);
|
||||||
},
|
},
|
||||||
|
|
||||||
timerFunc: function() {
|
timerFunc: function() {
|
||||||
|
|
|
@ -15,6 +15,12 @@ Vue.component('media-search', {
|
||||||
obj[type] = true;
|
obj[type] = true;
|
||||||
return obj;
|
return obj;
|
||||||
}, {}),
|
}, {}),
|
||||||
|
|
||||||
|
searchTypes: Object.keys(this.supportedTypes).reduce((obj, type) => {
|
||||||
|
if (type !== 'generic' && type !== 'base')
|
||||||
|
obj[type] = true;
|
||||||
|
return obj;
|
||||||
|
}, {}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -31,7 +37,7 @@ Vue.component('media-search', {
|
||||||
},
|
},
|
||||||
|
|
||||||
search: async function(event) {
|
search: async function(event) {
|
||||||
const types = Object.entries(this.types).filter(t => t[0] !== 'generic' && t[1]).map(t => t[0]);
|
const types = Object.entries(this.searchTypes).filter(t => t[1]).map(t => t[0]);
|
||||||
const protocol = this.isUrl(this.query);
|
const protocol = this.isUrl(this.query);
|
||||||
|
|
||||||
if (protocol) {
|
if (protocol) {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="col-3 item-container">
|
<div class="col-3 item-container">
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<span v-text="status.title"
|
<span v-text="status.title || status.url"
|
||||||
@click="bus.$emit('info-load', status.url)"
|
@click="bus.$emit('info-load', status.url)"
|
||||||
v-if="status.title"></span>
|
v-if="status.title"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,8 +5,12 @@
|
||||||
{% include 'plugins/media/info.html' %}
|
{% include 'plugins/media/info.html' %}
|
||||||
{% include 'plugins/media/subs.html' %}
|
{% include 'plugins/media/subs.html' %}
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/base.js') }}"></script>
|
||||||
|
|
||||||
{% for script in utils.search_directory(static_folder + '/js/plugins/media/handlers', 'js', recursive=True) %}
|
{% for script in utils.search_directory(static_folder + '/js/plugins/media/handlers', 'js', recursive=True) %}
|
||||||
|
{% if script != 'base.js' %}
|
||||||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/' + script) }}"></script>
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/' + script) }}"></script>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.css') }}">
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
<div class="info-container">
|
<div class="info-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3 attr">URL</div>
|
<div class="col-3 attr">URL</div>
|
||||||
<div class="col-9 value" v-text="item.url"></div>
|
<div class="col-9 value">
|
||||||
|
<a :href="item.url" v-text="item.url" target="_blank"></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item.title">
|
<div class="row" v-if="item.title">
|
||||||
|
@ -12,6 +14,26 @@
|
||||||
<div class="col-9 value" v-text="item.title"></div>
|
<div class="col-9 value" v-text="item.title"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item.description">
|
||||||
|
<div class="col-3 attr">Description</div>
|
||||||
|
<div class="col-9 value" v-text="item.description"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item.channelTitle">
|
||||||
|
<div class="col-3 attr">Channel</div>
|
||||||
|
<div class="col-9 value">
|
||||||
|
<a :href="'https://www.youtube.com/channel/' + item.channelId" v-if="item.channelId" target="_blank">
|
||||||
|
{% raw %}{{ item.channelTitle }}{% endraw %}
|
||||||
|
</a>
|
||||||
|
<span v-text="item.channelTitle" v-else></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item.publishedAt">
|
||||||
|
<div class="col-3 attr">Published</div>
|
||||||
|
<div class="col-9 value" v-text="new Date(item.publishedAt).toDateString()"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item.duration">
|
<div class="row" v-if="item.duration">
|
||||||
<div class="col-3 attr">Duration</div>
|
<div class="col-3 attr">Duration</div>
|
||||||
<div class="col-9 value" v-text="convertTime(item.duration)"></div>
|
<div class="col-9 value" v-text="convertTime(item.duration)"></div>
|
||||||
|
|
|
@ -18,11 +18,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row types fade-in" :class="{hidden: !showFilter}">
|
<div class="row types fade-in" :class="{hidden: !showFilter}">
|
||||||
<div class="type" v-for="config,type in types">
|
<div class="type" v-for="config,type in searchTypes">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="type"
|
name="type"
|
||||||
:id="'media-type-' + type"
|
:id="'media-type-' + type"
|
||||||
v-model.lazy="types[type]">
|
v-model.lazy="searchTypes[type]">
|
||||||
<label :for="'media-type-' + type" v-text="type"></label>
|
<label :for="'media-type-' + type" v-text="type"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import functools
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
@ -80,7 +81,7 @@ class MediaPlugin(Plugin):
|
||||||
:type media_dirs: list
|
:type media_dirs: list
|
||||||
|
|
||||||
:param download_dir: Directory where external resources/torrents will be
|
:param download_dir: Directory where external resources/torrents will be
|
||||||
downloaded (default: none)
|
downloaded (default: ~/Downloads)
|
||||||
:type download_dir: str
|
:type download_dir: str
|
||||||
|
|
||||||
:param env: Environment variables key-values to pass to the
|
:param env: Environment variables key-values to pass to the
|
||||||
|
@ -110,7 +111,6 @@ class MediaPlugin(Plugin):
|
||||||
raise AttributeError('No media plugin configured')
|
raise AttributeError('No media plugin configured')
|
||||||
|
|
||||||
media_dirs = media_dirs or player_config.get('media_dirs', [])
|
media_dirs = media_dirs or player_config.get('media_dirs', [])
|
||||||
download_dir = download_dir or player_config.get('download_dir')
|
|
||||||
|
|
||||||
if self.__class__.__name__ == 'MediaPlugin':
|
if self.__class__.__name__ == 'MediaPlugin':
|
||||||
# Populate this plugin with the actions of the configured player
|
# Populate this plugin with the actions of the configured player
|
||||||
|
@ -130,14 +130,14 @@ class MediaPlugin(Plugin):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if download_dir:
|
self.download_dir = os.path.abspath(os.path.expanduser(
|
||||||
self.download_dir = os.path.abspath(os.path.expanduser(download_dir))
|
download_dir or player_config.get('download_dir') or
|
||||||
|
os.path.join((os.environ['HOME'] or self._env.get('HOME') or '/'), 'Downloads')))
|
||||||
|
|
||||||
if not os.path.isdir(self.download_dir):
|
if not os.path.isdir(self.download_dir):
|
||||||
raise RuntimeError('download_dir [{}] is not a valid directory'
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
.format(self.download_dir))
|
|
||||||
|
|
||||||
self.media_dirs.add(self.download_dir)
|
self.media_dirs.add(self.download_dir)
|
||||||
|
|
||||||
self._is_playing_torrent = False
|
self._is_playing_torrent = False
|
||||||
self._videos_queue = []
|
self._videos_queue = []
|
||||||
|
|
||||||
|
@ -494,6 +494,30 @@ class MediaPlugin(Plugin):
|
||||||
).group(1).split(':')[::-1])]
|
).group(1).split(':')[::-1])]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def download(self, url, filename=None, directory=None):
|
||||||
|
"""
|
||||||
|
Download a media URL
|
||||||
|
|
||||||
|
:param url: Media URL
|
||||||
|
:param filename: Media filename (default: URL filename)
|
||||||
|
:param directory: Destination directory (default: download_dir)
|
||||||
|
:return: The absolute path to the downloaded file
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
filename = url.split('/')[-1]
|
||||||
|
if not directory:
|
||||||
|
directory = self.download_dir
|
||||||
|
|
||||||
|
path = os.path.join(directory, filename)
|
||||||
|
content = requests.get(url).content
|
||||||
|
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
return self._is_local
|
return self._is_local
|
||||||
|
|
||||||
|
@ -507,7 +531,6 @@ class MediaPlugin(Plugin):
|
||||||
if os.path.isfile(subtitles):
|
if os.path.isfile(subtitles):
|
||||||
return os.path.abspath(subtitles)
|
return os.path.abspath(subtitles)
|
||||||
else:
|
else:
|
||||||
import requests
|
|
||||||
content = requests.get(subtitles).content
|
content = requests.get(subtitles).content
|
||||||
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
|
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
|
||||||
suffix='.srt', delete=False)
|
suffix='.srt', delete=False)
|
||||||
|
|
|
@ -278,7 +278,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'duration': self._player.duration(),
|
'duration': self._player.duration(),
|
||||||
'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1],
|
'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None,
|
||||||
'fullscreen': self._player.fullscreen(),
|
'fullscreen': self._player.fullscreen(),
|
||||||
'mute': self._player._is_muted,
|
'mute': self._player._is_muted,
|
||||||
'path': self._player.get_source(),
|
'path': self._player.get_source(),
|
||||||
|
@ -286,7 +286,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
'position': self._player.position(),
|
'position': self._player.position(),
|
||||||
'seekable': self._player.can_seek(),
|
'seekable': self._player.can_seek(),
|
||||||
'state': state,
|
'state': state,
|
||||||
'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1],
|
'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None,
|
||||||
'url': self._player.get_source(),
|
'url': self._player.get_source(),
|
||||||
'volume': self._player.volume(),
|
'volume': self._player.volume(),
|
||||||
'volume_max': 100,
|
'volume_max': 100,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class MediaSearcher:
|
class MediaSearcher:
|
||||||
"""
|
"""
|
||||||
Base class for media searchers
|
Base class for media searchers
|
||||||
|
@ -8,7 +9,6 @@ class MediaSearcher:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
def search(self, query, *args, **kwargs):
|
def search(self, query, *args, **kwargs):
|
||||||
raise NotImplementedError('The search method should be implemented ' +
|
raise NotImplementedError('The search method should be implemented ' +
|
||||||
'by a derived class')
|
'by a derived class')
|
||||||
|
|
|
@ -215,6 +215,7 @@ class LocalMediaSearcher(MediaSearcher):
|
||||||
filter(MediaToken.token.in_(query_tokens)). \
|
filter(MediaToken.token.in_(query_tokens)). \
|
||||||
group_by(MediaFile.path). \
|
group_by(MediaFile.path). \
|
||||||
having(func.count(MediaFileToken.token_id) >= len(query_tokens)):
|
having(func.count(MediaFileToken.token_id) >= len(query_tokens)):
|
||||||
|
if (os.path.isfile(file_record.path)):
|
||||||
results[file_record.path] = {
|
results[file_record.path] = {
|
||||||
'url': 'file://' + file_record.path,
|
'url': 'file://' + file_record.path,
|
||||||
'title': os.path.basename(file_record.path),
|
'title': os.path.basename(file_record.path),
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import re
|
import re
|
||||||
import urllib
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.plugins.media.search import MediaSearcher
|
from platypush.plugins.media.search import MediaSearcher
|
||||||
|
|
||||||
class YoutubeMediaSearcher(MediaSearcher):
|
class YoutubeMediaSearcher(MediaSearcher):
|
||||||
def search(self, query):
|
def search(self, query, **kwargs):
|
||||||
"""
|
"""
|
||||||
Performs a YouTube search either using the YouTube API (faster and
|
Performs a YouTube search either using the YouTube API (faster and
|
||||||
recommended, it requires the :mod:`platypush.plugins.google.youtube`
|
recommended, it requires the :mod:`platypush.plugins.google.youtube`
|
||||||
|
@ -23,11 +24,12 @@ class YoutubeMediaSearcher(MediaSearcher):
|
||||||
|
|
||||||
return self._youtube_search_html_parse(query=query)
|
return self._youtube_search_html_parse(query=query)
|
||||||
|
|
||||||
def _youtube_search_api(self, query):
|
@staticmethod
|
||||||
|
def _youtube_search_api(query):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
|
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
|
||||||
'title': item.get('snippet', {}).get('title', '<No Title>'),
|
**item.get('snippet', {}),
|
||||||
}
|
}
|
||||||
for item in get_plugin('google.youtube').search(query=query).output
|
for item in get_plugin('google.youtube').search(query=query).output
|
||||||
if item.get('id', {}).get('kind') == 'youtube#video'
|
if item.get('id', {}).get('kind') == 'youtube#video'
|
||||||
|
|
|
@ -45,6 +45,8 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
self._default_fullscreen = fullscreen
|
self._default_fullscreen = fullscreen
|
||||||
self._default_volume = volume
|
self._default_volume = volume
|
||||||
self._on_stop_callbacks = []
|
self._on_stop_callbacks = []
|
||||||
|
self._title = None
|
||||||
|
self._filename = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _watched_event_types(cls):
|
def _watched_event_types(cls):
|
||||||
|
@ -76,6 +78,9 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
|
|
||||||
def _reset_state(self):
|
def _reset_state(self):
|
||||||
self._latest_seek = None
|
self._latest_seek = None
|
||||||
|
self._title = None
|
||||||
|
self._filename = None
|
||||||
|
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.release()
|
self._player.release()
|
||||||
self._player = None
|
self._player = None
|
||||||
|
@ -105,8 +110,12 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
for cbk in self._on_stop_callbacks:
|
for cbk in self._on_stop_callbacks:
|
||||||
cbk()
|
cbk()
|
||||||
elif event.type == EventType.MediaPlayerTitleChanged:
|
elif event.type == EventType.MediaPlayerTitleChanged:
|
||||||
|
self._filename = event.u.filename
|
||||||
|
self._title = event.u.new_title
|
||||||
self._post_event(NewPlayingMediaEvent, resource=event.u.new_title)
|
self._post_event(NewPlayingMediaEvent, resource=event.u.new_title)
|
||||||
elif event.type == EventType.MediaPlayerMediaChanged:
|
elif event.type == EventType.MediaPlayerMediaChanged:
|
||||||
|
self._filename = event.u.filename
|
||||||
|
self._title = event.u.new_title
|
||||||
self._post_event(NewPlayingMediaEvent, resource=event.u.filename)
|
self._post_event(NewPlayingMediaEvent, resource=event.u.filename)
|
||||||
elif event.type == EventType.MediaPlayerLengthChanged:
|
elif event.type == EventType.MediaPlayerLengthChanged:
|
||||||
self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource())
|
self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource())
|
||||||
|
@ -396,8 +405,8 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
status['path'] = status['url']
|
status['path'] = status['url']
|
||||||
status['pause'] = status['state'] == PlayerState.PAUSE.value
|
status['pause'] = status['state'] == PlayerState.PAUSE.value
|
||||||
status['percent_pos'] = self._player.get_position()*100
|
status['percent_pos'] = self._player.get_position()*100
|
||||||
status['filename'] = urllib.parse.unquote(status['url']).split('/')[-1]
|
status['filename'] = self._filename
|
||||||
status['title'] = status['filename']
|
status['title'] = self._title
|
||||||
status['volume'] = self._player.audio_get_volume()
|
status['volume'] = self._player.audio_get_volume()
|
||||||
status['volume_max'] = 100
|
status['volume_max'] = 100
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue