forked from platypush/platypush
New media webplugin WIP
This commit is contained in:
parent
ba800ef8e2
commit
9305f86d0c
|
@ -8,6 +8,7 @@
|
||||||
@import 'webpanel/plugins/media/devices';
|
@import 'webpanel/plugins/media/devices';
|
||||||
@import 'webpanel/plugins/media/results';
|
@import 'webpanel/plugins/media/results';
|
||||||
@import 'webpanel/plugins/media/controls';
|
@import 'webpanel/plugins/media/controls';
|
||||||
|
@import 'webpanel/plugins/media/info';
|
||||||
|
|
||||||
.media-plugin {
|
.media-plugin {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
.media-plugin {
|
||||||
|
.info-container {
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .75em .5em;
|
||||||
|
border-bottom: $default-border-2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attr {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ MediaHandlers.file = Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
text: 'Download',
|
text: 'Download (on client)',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
action: this.download,
|
action: this.download,
|
||||||
},
|
},
|
||||||
|
@ -42,21 +42,46 @@ MediaHandlers.file = Vue.extend({
|
||||||
return !!url.match('^(file://)?/');
|
return !!url.match('^(file://)?/');
|
||||||
},
|
},
|
||||||
|
|
||||||
getMetadata: function(url) {
|
getMetadata: async function(item, onlyBase=false) {
|
||||||
return {
|
if (typeof item === 'string') {
|
||||||
url: url,
|
item = {
|
||||||
title: url.split('/').pop(),
|
url: item,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.path)
|
||||||
|
item.path = item.url.startsWith('file://') ? item.url.substr(7) : item.url;
|
||||||
|
|
||||||
|
if (!item.title)
|
||||||
|
item.title = item.path.split('/').pop();
|
||||||
|
|
||||||
|
if (!item.size && !onlyBase)
|
||||||
|
item.size = await request('file.getsize', {filename: item.path});
|
||||||
|
|
||||||
|
if (!item.duration && !onlyBase)
|
||||||
|
item.duration = await request('media.get_media_file_duration', {filename: item.path});
|
||||||
|
|
||||||
|
return item;
|
||||||
},
|
},
|
||||||
|
|
||||||
play: function(item) {
|
play: function(item) {
|
||||||
this.bus.$emit('play', item);
|
this.bus.$emit('play', item);
|
||||||
},
|
},
|
||||||
|
|
||||||
download: function(item) {
|
download: async function(item) {
|
||||||
|
this.bus.$on('streaming-started', (media) => {
|
||||||
|
if (media.resource === item.url) {
|
||||||
|
this.bus.$off('streaming-started');
|
||||||
|
window.open(media.url + '?download', '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('start-streaming', item.url);
|
||||||
},
|
},
|
||||||
|
|
||||||
info: function(item) {
|
info: async function(item) {
|
||||||
|
this.bus.$emit('info-loading');
|
||||||
|
this.bus.$emit('info', (await this.getMetadata(item)));
|
||||||
},
|
},
|
||||||
|
|
||||||
searchSubtitles: function(item) {
|
searchSubtitles: function(item) {
|
||||||
|
@ -74,12 +99,16 @@ MediaHandlers.generic = MediaHandlers.file.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getMetadata: function(url) {
|
getMetadata: async function(url) {
|
||||||
return {
|
return {
|
||||||
url: url,
|
url: url,
|
||||||
title: url,
|
title: url,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
info: async function(item) {
|
||||||
|
this.bus.$emit('info', (await this.getMetadata(item)));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ MediaHandlers.torrent = Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
text: 'Download',
|
text: 'Download (on server)',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
action: this.download,
|
action: this.download,
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ MediaHandlers.youtube = Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
text: 'Download',
|
text: 'Download (on server)',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
action: this.download,
|
action: this.download,
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,16 @@ const mediaUtils = {
|
||||||
ret.push(t.m, t.s);
|
ret.push(t.m, t.s);
|
||||||
return ret.join(':');
|
return ret.join(':');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
convertSize: function(size) {
|
||||||
|
size = parseInt(size); // Normalize strings
|
||||||
|
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let s=size, i=0;
|
||||||
|
|
||||||
|
for (; s > 1024 && i < units.length; i++, s = parseInt(s/1024));
|
||||||
|
return (size / Math.pow(2, 10*i)).toFixed(2) + ' ' + units[i];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,10 +48,17 @@ Vue.component('media', {
|
||||||
results: [],
|
results: [],
|
||||||
status: {},
|
status: {},
|
||||||
selectedDevice: {},
|
selectedDevice: {},
|
||||||
|
|
||||||
loading: {
|
loading: {
|
||||||
results: false,
|
results: false,
|
||||||
media: false,
|
media: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
infoModal: {
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
item: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -52,14 +69,11 @@ Vue.component('media', {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
refresh: async function() {
|
|
||||||
},
|
|
||||||
|
|
||||||
onResultsLoading: function() {
|
onResultsLoading: function() {
|
||||||
this.loading.results = true;
|
this.loading.results = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
onResultsReady: function(results) {
|
onResultsReady: async function(results) {
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (result.type && MediaHandlers[result.type]) {
|
if (result.type && MediaHandlers[result.type]) {
|
||||||
result.handler = MediaHandlers[result.type];
|
result.handler = MediaHandlers[result.type];
|
||||||
|
@ -76,7 +90,7 @@ Vue.component('media', {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(result.handler.getMetadata(result.url)).forEach(entry => {
|
Object.entries(await result.handler.getMetadata(result, onlyBase=true)).forEach(entry => {
|
||||||
Vue.set(result, entry[0], entry[1]);
|
Vue.set(result, entry[0], entry[1]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -131,14 +145,31 @@ Vue.component('media', {
|
||||||
},
|
},
|
||||||
|
|
||||||
info: function(item) {
|
info: function(item) {
|
||||||
// TODO
|
for (const [attr, value] of Object.entries(item)) {
|
||||||
console.log(item);
|
Vue.set(this.infoModal.item, attr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.infoModal.loading = false;
|
||||||
|
this.infoModal.visible = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
infoLoading: function() {
|
||||||
|
this.infoModal.loading = true;
|
||||||
|
this.infoModal.visible = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
startStreaming: async function(item) {
|
startStreaming: async function(item) {
|
||||||
return await request('media.start_streaming', {
|
const resource = item instanceof Object ? item.url : item;
|
||||||
media: item.url,
|
const ret = await request('media.start_streaming', {
|
||||||
|
media: resource,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('streaming-started', {
|
||||||
|
url: ret.url,
|
||||||
|
resource: resource,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
},
|
},
|
||||||
|
|
||||||
selectDevice: async function(device) {
|
selectDevice: async function(device) {
|
||||||
|
@ -199,8 +230,6 @@ Vue.component('media', {
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
this.refresh();
|
|
||||||
|
|
||||||
for (const [type, Handler] of Object.entries(MediaHandlers)) {
|
for (const [type, Handler] of Object.entries(MediaHandlers)) {
|
||||||
MediaHandlers[type] = new Handler();
|
MediaHandlers[type] = new Handler();
|
||||||
MediaHandlers[type].bus = this.bus;
|
MediaHandlers[type].bus = this.bus;
|
||||||
|
@ -219,10 +248,12 @@ Vue.component('media', {
|
||||||
this.bus.$on('seek', this.seek);
|
this.bus.$on('seek', this.seek);
|
||||||
this.bus.$on('volume', this.setVolume);
|
this.bus.$on('volume', this.setVolume);
|
||||||
this.bus.$on('info', this.info);
|
this.bus.$on('info', this.info);
|
||||||
|
this.bus.$on('info-loading', this.infoLoading);
|
||||||
this.bus.$on('selected-device', this.selectDevice);
|
this.bus.$on('selected-device', this.selectDevice);
|
||||||
this.bus.$on('results-loading', this.onResultsLoading);
|
this.bus.$on('results-loading', this.onResultsLoading);
|
||||||
this.bus.$on('results-ready', this.onResultsReady);
|
this.bus.$on('results-ready', this.onResultsReady);
|
||||||
this.bus.$on('status-update', this.onStatusUpdate);
|
this.bus.$on('status-update', this.onStatusUpdate);
|
||||||
|
this.bus.$on('start-streaming', this.startStreaming);
|
||||||
|
|
||||||
setInterval(this.timerFunc, 1000);
|
setInterval(this.timerFunc, 1000);
|
||||||
},
|
},
|
||||||
|
|
13
platypush/backend/http/static/js/plugins/media/info.js
Normal file
13
platypush/backend/http/static/js/plugins/media/info.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Vue.component('media-info', {
|
||||||
|
template: '#tmpl-media-info',
|
||||||
|
mixins: [mediaUtils],
|
||||||
|
props: {
|
||||||
|
bus: { type: Object },
|
||||||
|
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ MediaPlayers.browser = Vue.extend({
|
||||||
default: () => {
|
default: () => {
|
||||||
return {
|
return {
|
||||||
youtube: true,
|
youtube: true,
|
||||||
|
generic: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,7 @@ MediaPlayers.chromecast = Vue.extend({
|
||||||
default: () => {
|
default: () => {
|
||||||
return {
|
return {
|
||||||
youtube: true,
|
youtube: true,
|
||||||
|
generic: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@ MediaPlayers.local = Vue.extend({
|
||||||
return {
|
return {
|
||||||
file: true,
|
file: true,
|
||||||
youtube: true,
|
youtube: true,
|
||||||
|
generic: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,14 +60,6 @@ Vue.component('media-results', {
|
||||||
this.selectedItem = item;
|
this.selectedItem = item;
|
||||||
openDropdown(this.$refs.mediaItemDropdown);
|
openDropdown(this.$refs.mediaItemDropdown);
|
||||||
},
|
},
|
||||||
|
|
||||||
play: function(item) {
|
|
||||||
this.bus.$emit('play', item);
|
|
||||||
},
|
|
||||||
|
|
||||||
info: function(item) {
|
|
||||||
this.bus.$emit('info', item);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
{% include 'plugins/media/controls.html' %}
|
{% include 'plugins/media/controls.html' %}
|
||||||
{% include 'plugins/media/results.html' %}
|
{% include 'plugins/media/results.html' %}
|
||||||
{% include 'plugins/media/item.html' %}
|
{% include 'plugins/media/item.html' %}
|
||||||
|
{% include 'plugins/media/info.html' %}
|
||||||
|
|
||||||
|
|
||||||
{% 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) %}
|
||||||
<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>
|
||||||
|
@ -35,6 +37,11 @@
|
||||||
:status="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] ? status[selectedDevice.type][selectedDevice.name] : {}"
|
:status="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] ? status[selectedDevice.type][selectedDevice.name] : {}"
|
||||||
v-if="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] && (status[selectedDevice.type][selectedDevice.name].state === 'play' || status[selectedDevice.type][selectedDevice.name].state === 'pause')">
|
v-if="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] && (status[selectedDevice.type][selectedDevice.name].state === 'play' || status[selectedDevice.type][selectedDevice.name].state === 'pause')">
|
||||||
</media-controls>
|
</media-controls>
|
||||||
|
|
||||||
|
<modal id="media-info" title="Media info" v-model="infoModal.visible" ref="modalInfo">
|
||||||
|
<div class="loading" v-if="infoModal.loading">Loading</div>
|
||||||
|
<media-info :bus="bus" :item="infoModal.item" v-else></media-info>
|
||||||
|
</modal>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
26
platypush/backend/http/templates/plugins/media/info.html
Normal file
26
platypush/backend/http/templates/plugins/media/info.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/info.js') }}"></script>
|
||||||
|
|
||||||
|
<script type="text/x-template" id="tmpl-media-info">
|
||||||
|
<div class="info-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 attr">URL</div>
|
||||||
|
<div class="col-9 value" v-text="item.url"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item.title">
|
||||||
|
<div class="col-3 attr">Title</div>
|
||||||
|
<div class="col-9 value" v-text="item.title"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item.duration">
|
||||||
|
<div class="col-3 attr">Duration</div>
|
||||||
|
<div class="col-9 value" v-text="convertTime(item.duration)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item.size">
|
||||||
|
<div class="col-3 attr">Size</div>
|
||||||
|
<div class="col-9 value" v-text="convertSize(item.size)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
|
@ -54,5 +54,12 @@ class FilePlugin(Plugin):
|
||||||
with open(self._get_path(filename), 'a') as f:
|
with open(self._get_path(filename), 'a') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def getsize(self, filename):
|
||||||
|
"""
|
||||||
|
Get the size of the specified filename in bytes
|
||||||
|
"""
|
||||||
|
return os.path.getsize(filename)
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,11 @@
|
||||||
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
|
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.google import GooglePlugin
|
from platypush.plugins.google import GooglePlugin
|
||||||
from platypush.plugins.calendar import CalendarInterface
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleYoutubePlugin(GooglePlugin, CalendarInterface):
|
class GoogleYoutubePlugin(GooglePlugin):
|
||||||
"""
|
"""
|
||||||
YouTube plugin
|
YouTube plugin
|
||||||
"""
|
"""
|
||||||
|
@ -24,31 +19,33 @@ class GoogleYoutubePlugin(GooglePlugin, CalendarInterface):
|
||||||
# See https://developers.google.com/youtube/v3/getting-started#resources
|
# See https://developers.google.com/youtube/v3/getting-started#resources
|
||||||
_default_types = ['video']
|
_default_types = ['video']
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(scopes=self.scopes, *args, **kwargs)
|
super().__init__(scopes=self.scopes, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def search(self, parts=None, query='', types=None, max_results=25, **kwargs):
|
def search(self, parts=None, query='', types=None, max_results=25, **kwargs):
|
||||||
"""
|
"""
|
||||||
Search for YouTube content.
|
Search for YouTube content.
|
||||||
|
|
||||||
:param parts: List of parts to get (default: snippet). See the `YouTube API documentation <https://developers.google.com/youtube/v3/getting-started#part>`_.
|
:param parts: List of parts to get (default: snippet). See the `YouTube API documentation
|
||||||
|
<https://developers.google.com/youtube/v3/getting-started#part>`_.
|
||||||
:type parts: list[str] or str
|
:type parts: list[str] or str
|
||||||
|
|
||||||
:param query: Query string (default: empty string)
|
:param query: Query string (default: empty string)
|
||||||
:type query: str
|
:type query: str
|
||||||
|
|
||||||
:param types: List of types to retrieve (default: video). See the `YouTube API documentation <https://developers.google.com/youtube/v3/getting-started#resources>`_.
|
:param types: List of types to retrieve (default: video). See the `YouTube API documentation
|
||||||
|
<https://developers.google.com/youtube/v3/getting-started#resources>`_.
|
||||||
:type types: list[str] or str
|
:type types: list[str] or str
|
||||||
|
|
||||||
:param max_results: Maximum number of items that will be returned (default: 25).
|
:param max_results: Maximum number of items that will be returned (default: 25).
|
||||||
:type max_results: int
|
:type max_results: int
|
||||||
|
|
||||||
:param kwargs: Any extra arguments that will be transparently passed to the YouTube API, see the `YouTube API documentation <https://developers.google.com/youtube/v3/docs/search/list#parameters>`_.
|
:param kwargs: Any extra arguments that will be transparently passed to the YouTube API, see the
|
||||||
|
`YouTube API documentation <https://developers.google.com/youtube/v3/docs/search/list#parameters>`_.
|
||||||
|
|
||||||
:return: A list of YouTube resources, see the `YouTube API documentation <https://developers.google.com/youtube/v3/docs/search#resource>`_.
|
:return: A list of YouTube resources, see the `YouTube API documentation
|
||||||
|
<https://developers.google.com/youtube/v3/docs/search#resource>`_.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parts = parts or self._default_parts[:]
|
parts = parts or self._default_parts[:]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import enum
|
import enum
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
|
@ -28,10 +29,12 @@ class MediaPlugin(Plugin):
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast)
|
* A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast)
|
||||||
* The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent (recommended)
|
* The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent
|
||||||
|
(recommended)
|
||||||
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support over native library
|
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support over native library
|
||||||
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
|
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
|
||||||
* **requests** (``pip install requests``), optional, for local files over HTTP streaming supporting
|
* **requests** (``pip install requests``), optional, for local files over HTTP streaming supporting
|
||||||
|
* **ffmpeg**,optional, to get media files metadata
|
||||||
|
|
||||||
To start the local media stream service over HTTP you will also need the
|
To start the local media stream service over HTTP you will also need the
|
||||||
:class:`platypush.backend.http.HttpBackend` backend enabled.
|
:class:`platypush.backend.http.HttpBackend` backend enabled.
|
||||||
|
@ -154,12 +157,12 @@ class MediaPlugin(Plugin):
|
||||||
# The Chromecast has already its native way to handle YouTube
|
# The Chromecast has already its native way to handle YouTube
|
||||||
return resource
|
return resource
|
||||||
|
|
||||||
resource = self._get_youtube_content(resource)
|
resource = self.get_youtube_url(resource).output
|
||||||
elif resource.startswith('magnet:?'):
|
elif resource.startswith('magnet:?'):
|
||||||
try:
|
try:
|
||||||
get_plugin('media.webtorrent')
|
get_plugin('media.webtorrent')
|
||||||
return resource # media.webtorrent will handle this
|
return resource # media.webtorrent will handle this
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
torrents = get_plugin('torrent')
|
torrents = get_plugin('torrent')
|
||||||
|
@ -448,20 +451,49 @@ class MediaPlugin(Plugin):
|
||||||
html = ''
|
html = ''
|
||||||
|
|
||||||
self.logger.info('{} YouTube video results for the search query "{}"'
|
self.logger.info('{} YouTube video results for the search query "{}"'
|
||||||
.format(len(results), query))
|
.format(len(results), query))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@action
|
||||||
def _get_youtube_content(cls, url):
|
def get_youtube_url(self, url):
|
||||||
m = re.match('youtube:video:(.*)', url)
|
m = re.match('youtube:video:(.*)', url)
|
||||||
if m: url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
|
if m:
|
||||||
|
url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
|
||||||
proc = subprocess.Popen(['youtube-dl','-f','best', '-g', url],
|
|
||||||
stdout=subprocess.PIPE)
|
|
||||||
|
|
||||||
|
proc = subprocess.Popen(['youtube-dl', '-f', 'best', '-g', url], stdout=subprocess.PIPE)
|
||||||
return proc.stdout.read().decode("utf-8", "strict")[:-1]
|
return proc.stdout.read().decode("utf-8", "strict")[:-1]
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_youtube_info(self, url):
|
||||||
|
m = re.match('youtube:video:(.*)', url)
|
||||||
|
if m:
|
||||||
|
url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
|
||||||
|
|
||||||
|
proc = subprocess.Popen(['youtube-dl', '-j', url], stdout=subprocess.PIPE)
|
||||||
|
return proc.stdout.read().decode("utf-8", "strict")[:-1]
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_media_file_duration(self, filename):
|
||||||
|
"""
|
||||||
|
Get the duration of a media file in seconds. Requires ffmpeg
|
||||||
|
"""
|
||||||
|
|
||||||
|
if filename.startswith('file://'):
|
||||||
|
filename = filename[7:]
|
||||||
|
|
||||||
|
result = subprocess.Popen(["ffprobe", filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
return functools.reduce(
|
||||||
|
lambda t, t_i: t + t_i,
|
||||||
|
[float(t) * pow(60, i) for (i, t) in enumerate(re.search(
|
||||||
|
'^Duration:\s*([^,]+)', [x.decode()
|
||||||
|
for x in result.stdout.readlines()
|
||||||
|
if "Duration" in x.decode()]
|
||||||
|
.pop().strip()
|
||||||
|
).group(1).split(':')[::-1])]
|
||||||
|
)
|
||||||
|
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
return self._is_local
|
return self._is_local
|
||||||
|
|
||||||
|
|
|
@ -43,14 +43,15 @@ class LocalMediaSearcher(MediaSearcher):
|
||||||
if not self._db_engine:
|
if not self._db_engine:
|
||||||
self._db_engine = create_engine(
|
self._db_engine = create_engine(
|
||||||
'sqlite:///{}'.format(self.db_file),
|
'sqlite:///{}'.format(self.db_file),
|
||||||
connect_args = { 'check_same_thread': False })
|
connect_args = {'check_same_thread': False})
|
||||||
|
|
||||||
Base.metadata.create_all(self._db_engine)
|
Base.metadata.create_all(self._db_engine)
|
||||||
Session.configure(bind=self._db_engine)
|
Session.configure(bind=self._db_engine)
|
||||||
|
|
||||||
return Session()
|
return Session()
|
||||||
|
|
||||||
def _get_or_create_dir_entry(self, session, path):
|
@staticmethod
|
||||||
|
def _get_or_create_dir_entry(session, path):
|
||||||
record = session.query(MediaDirectory).filter_by(path=path).first()
|
record = session.query(MediaDirectory).filter_by(path=path).first()
|
||||||
if record is None:
|
if record is None:
|
||||||
record = MediaDirectory.build(path=path)
|
record = MediaDirectory.build(path=path)
|
||||||
|
@ -103,13 +104,11 @@ class LocalMediaSearcher(MediaSearcher):
|
||||||
return session.query(MediaToken).filter(
|
return session.query(MediaToken).filter(
|
||||||
MediaToken.token.in_(tokens)).all()
|
MediaToken.token.in_(tokens)).all()
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_records(cls, dir_record, session):
|
def _get_file_records(cls, dir_record, session):
|
||||||
return session.query(MediaFile).filter_by(
|
return session.query(MediaFile).filter_by(
|
||||||
directory_id=dir_record.id).all()
|
directory_id=dir_record.id).all()
|
||||||
|
|
||||||
|
|
||||||
def scan(self, media_dir, session=None, dir_record=None):
|
def scan(self, media_dir, session=None, dir_record=None):
|
||||||
"""
|
"""
|
||||||
Scans a media directory and stores the search results in the internal
|
Scans a media directory and stores the search results in the internal
|
||||||
|
@ -186,8 +185,7 @@ class LocalMediaSearcher(MediaSearcher):
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
def search(self, query, **kwargs):
|
||||||
def search(self, query):
|
|
||||||
"""
|
"""
|
||||||
Searches in the configured media directories given a query. It uses the
|
Searches in the configured media directories given a query. It uses the
|
||||||
built-in SQLite index if available. If any directory has changed since
|
built-in SQLite index if available. If any directory has changed since
|
||||||
|
@ -216,10 +214,12 @@ class LocalMediaSearcher(MediaSearcher):
|
||||||
join(MediaToken). \
|
join(MediaToken). \
|
||||||
filter(MediaToken.token.in_(query_tokens)). \
|
filter(MediaToken.token.in_(query_tokens)). \
|
||||||
group_by(MediaFile.path). \
|
group_by(MediaFile.path). \
|
||||||
order_by(func.count(MediaFileToken.token_id).desc()):
|
having(func.count(MediaFileToken.token_id) >= len(query_tokens)):
|
||||||
|
# order_by(func.count(MediaFileToken.token_id).desc()):
|
||||||
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),
|
||||||
|
'size': os.path.getsize(file_record.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.values()
|
return results.values()
|
||||||
|
|
|
@ -8,7 +8,7 @@ class TorrentMediaSearcher(MediaSearcher):
|
||||||
torrents = get_plugin('torrent')
|
torrents = get_plugin('torrent')
|
||||||
if not torrents:
|
if not torrents:
|
||||||
raise RuntimeError('Torrent plugin not available/configured')
|
raise RuntimeError('Torrent plugin not available/configured')
|
||||||
return torrents.search(query).output
|
return torrents.search(query, ).output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue