From e55735f4095bbad7c40bf55524d2dd302940592b Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <blacklight86@gmail.com>
Date: Tue, 25 Jun 2019 22:46:25 +0200
Subject: [PATCH] Added YouTube support to new media webplugin

---
 .../source/webpanel/plugins/media/info.scss   |  6 ++
 .../static/js/plugins/media/handlers/base.js  | 64 +++++++++++++++++++
 .../static/js/plugins/media/handlers/file.js  | 53 ++++++---------
 .../js/plugins/media/handlers/youtube.js      | 57 +++++++++++++++--
 .../http/static/js/plugins/media/index.js     | 25 ++++++--
 .../http/static/js/plugins/media/search.js    |  8 ++-
 .../templates/plugins/media/controls.html     |  2 +-
 .../http/templates/plugins/media/index.html   |  4 ++
 .../http/templates/plugins/media/info.html    | 24 ++++++-
 .../http/templates/plugins/media/search.html  |  4 +-
 platypush/plugins/media/__init__.py           | 41 +++++++++---
 platypush/plugins/media/omxplayer.py          |  4 +-
 platypush/plugins/media/search/__init__.py    |  2 +-
 platypush/plugins/media/search/local.py       | 11 ++--
 platypush/plugins/media/search/youtube.py     | 12 ++--
 platypush/plugins/media/vlc.py                | 13 +++-
 16 files changed, 256 insertions(+), 74 deletions(-)
 create mode 100644 platypush/backend/http/static/js/plugins/media/handlers/base.js

diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
index 294e073e..620d1841 100644
--- a/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
@@ -1,4 +1,10 @@
 .media-plugin {
+    #media-info {
+        .modal {
+            max-width: 90%;
+        }
+    }
+
     .info-container {
         .row {
             display: flex;
diff --git a/platypush/backend/http/static/js/plugins/media/handlers/base.js b/platypush/backend/http/static/js/plugins/media/handlers/base.js
new file mode 100644
index 00000000..b6e50544
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/media/handlers/base.js
@@ -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);
+    },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/media/handlers/file.js b/platypush/backend/http/static/js/plugins/media/handlers/file.js
index 47262b8d..28aa1848 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/file.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/file.js
@@ -1,6 +1,5 @@
-MediaHandlers.file = Vue.extend({
+MediaHandlers.file = MediaHandlers.base.extend({
     props: {
-        bus: { type: Object },
         iconClass: {
             type: String,
             default: 'fa fa-hdd',
@@ -64,10 +63,6 @@ MediaHandlers.file = Vue.extend({
             return item;
         },
 
-        play: function(item) {
-            this.bus.$emit('play', item);
-        },
-
         download: async function(item) {
             this.bus.$on('streaming-started', (media) => {
                 if (media.resource === item.url) {
@@ -78,41 +73,35 @@ MediaHandlers.file = Vue.extend({
 
             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({
     props: {
-        bus: { type: Object },
         iconClass: {
             type: String,
             default: 'fa fa-globe',
         },
     },
 
+    computed: {
+        dropdownItems: function() {
+            return [
+                {
+                    text: 'Play',
+                    icon: 'play',
+                    action: this.play,
+                },
+
+                {
+                    text: 'View info',
+                    icon: 'info',
+                    action: this.info,
+                },
+            ];
+        },
+    },
+
     methods: {
         getMetadata: async function(url) {
             return {
@@ -120,10 +109,6 @@ MediaHandlers.generic = MediaHandlers.file.extend({
                 title: url,
             };
         },
-
-        info: async function(item) {
-            this.bus.$emit('info', (await this.getMetadata(item)));
-        },
     },
 });
 
diff --git a/platypush/backend/http/static/js/plugins/media/handlers/youtube.js b/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
index 08a880bb..6ea9e07c 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
@@ -1,6 +1,5 @@
-MediaHandlers.youtube = Vue.extend({
+MediaHandlers.youtube = MediaHandlers.base.extend({
     props: {
-        bus: { type: Object },
         iconClass: {
             type: String,
             default: 'fab fa-youtube',
@@ -19,7 +18,13 @@ MediaHandlers.youtube = Vue.extend({
                 {
                     text: 'Download (on server)',
                     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) {
-            // TODO
             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');
         },
     },
 });
diff --git a/platypush/backend/http/static/js/plugins/media/index.js b/platypush/backend/http/static/js/plugins/media/index.js
index 6005820e..c1e4b522 100644
--- a/platypush/backend/http/static/js/plugins/media/index.js
+++ b/platypush/backend/http/static/js/plugins/media/index.js
@@ -110,6 +110,9 @@ Vue.component('media', {
 
             let status = await this.selectedDevice.play(item.url, item.subtitles);
 
+            if (item.title)
+                status.title = item.title;
+
             this.subsModal.visible = false;
             this.onStatusUpdate({
                 device: this.selectedDevice,
@@ -207,6 +210,10 @@ Vue.component('media', {
             const status = event.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])
                 Vue.set(this.status, dev.type, {});
             Vue.set(this.status[dev.type], dev.name, status);
@@ -224,10 +231,20 @@ Vue.component('media', {
             if (event.plugin.startsWith('media.'))
                 event.plugin = event.plugin.substr(6);
 
-            if (this.status[event.player] && this.status[event.player][event.plugin])
-                Vue.set(this.status[event.player], event.plugin, status);
-            else if (this.status[event.plugin] && this.status[event.plugin][event.player])
-                Vue.set(this.status[event.plugin], event.player, status);
+            var type, player;
+            if (this.status[event.player] && this.status[event.player][event.plugin]) {
+                type = event.player;
+                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() {
diff --git a/platypush/backend/http/static/js/plugins/media/search.js b/platypush/backend/http/static/js/plugins/media/search.js
index 15520f40..8b7f5a16 100644
--- a/platypush/backend/http/static/js/plugins/media/search.js
+++ b/platypush/backend/http/static/js/plugins/media/search.js
@@ -15,6 +15,12 @@ Vue.component('media-search', {
                 obj[type] = true;
                 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) {
-            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);
 
             if (protocol) {
diff --git a/platypush/backend/http/templates/plugins/media/controls.html b/platypush/backend/http/templates/plugins/media/controls.html
index 268afb46..90205177 100644
--- a/platypush/backend/http/templates/plugins/media/controls.html
+++ b/platypush/backend/http/templates/plugins/media/controls.html
@@ -4,7 +4,7 @@
     <div class="controls">
         <div class="col-3 item-container">
             <div class="item-info">
-                <span v-text="status.title"
+                <span v-text="status.title || status.url"
                       @click="bus.$emit('info-load', status.url)"
                       v-if="status.title"></span>
             </div>
diff --git a/platypush/backend/http/templates/plugins/media/index.html b/platypush/backend/http/templates/plugins/media/index.html
index d5e51103..51c759cd 100644
--- a/platypush/backend/http/templates/plugins/media/index.html
+++ b/platypush/backend/http/templates/plugins/media/index.html
@@ -5,8 +5,12 @@
 {% include 'plugins/media/info.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) %}
+    {% if script != 'base.js' %}
 <script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/' + script) }}"></script>
+    {% endif %}
 {% endfor %}
 
 <link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.css') }}">
diff --git a/platypush/backend/http/templates/plugins/media/info.html b/platypush/backend/http/templates/plugins/media/info.html
index 8de91a03..f98b7a02 100644
--- a/platypush/backend/http/templates/plugins/media/info.html
+++ b/platypush/backend/http/templates/plugins/media/info.html
@@ -4,7 +4,9 @@
     <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 class="col-9 value">
+                <a :href="item.url" v-text="item.url" target="_blank"></a>
+            </div>
         </div>
 
         <div class="row" v-if="item.title">
@@ -12,6 +14,26 @@
             <div class="col-9 value" v-text="item.title"></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="col-3 attr">Duration</div>
             <div class="col-9 value" v-text="convertTime(item.duration)"></div>
diff --git a/platypush/backend/http/templates/plugins/media/search.html b/platypush/backend/http/templates/plugins/media/search.html
index bb1f4495..c759b889 100644
--- a/platypush/backend/http/templates/plugins/media/search.html
+++ b/platypush/backend/http/templates/plugins/media/search.html
@@ -18,11 +18,11 @@
         </div>
 
         <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"
                        name="type"
                        :id="'media-type-' + type"
-                       v-model.lazy="types[type]">
+                       v-model.lazy="searchTypes[type]">
                 <label :for="'media-type-' + type" v-text="type"></label>
             </div>
         </div>
diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py
index 2b6835cf..11e9b3dc 100644
--- a/platypush/plugins/media/__init__.py
+++ b/platypush/plugins/media/__init__.py
@@ -3,6 +3,7 @@ import functools
 import os
 import queue
 import re
+import requests
 import subprocess
 import tempfile
 import threading
@@ -80,7 +81,7 @@ class MediaPlugin(Plugin):
         :type media_dirs: list
 
         :param download_dir: Directory where external resources/torrents will be
-            downloaded (default: none)
+            downloaded (default: ~/Downloads)
         :type download_dir: str
 
         :param env: Environment variables key-values to pass to the
@@ -110,7 +111,6 @@ class MediaPlugin(Plugin):
             raise AttributeError('No media plugin configured')
 
         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':
             # 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(download_dir))
-            if not os.path.isdir(self.download_dir):
-                raise RuntimeError('download_dir [{}] is not a valid directory'
-                                   .format(self.download_dir))
+        self.download_dir = os.path.abspath(os.path.expanduser(
+            download_dir or player_config.get('download_dir') or
+            os.path.join((os.environ['HOME'] or self._env.get('HOME') or '/'), 'Downloads')))
 
-            self.media_dirs.add(self.download_dir)
+        if not os.path.isdir(self.download_dir):
+            os.makedirs(self.download_dir, exist_ok=True)
 
+        self.media_dirs.add(self.download_dir)
         self._is_playing_torrent = False
         self._videos_queue = []
 
@@ -494,6 +494,30 @@ class MediaPlugin(Plugin):
             ).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):
         return self._is_local
 
@@ -507,7 +531,6 @@ class MediaPlugin(Plugin):
         if os.path.isfile(subtitles):
             return os.path.abspath(subtitles)
         else:
-            import requests
             content = requests.get(subtitles).content
             f = tempfile.NamedTemporaryFile(prefix='media_subs_',
                                             suffix='.srt', delete=False)
diff --git a/platypush/plugins/media/omxplayer.py b/platypush/plugins/media/omxplayer.py
index d78a7f85..1c4df641 100644
--- a/platypush/plugins/media/omxplayer.py
+++ b/platypush/plugins/media/omxplayer.py
@@ -278,7 +278,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
 
             return {
                 '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(),
                 'mute': self._player._is_muted,
                 'path': self._player.get_source(),
@@ -286,7 +286,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
                 'position': self._player.position(),
                 'seekable': self._player.can_seek(),
                 '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(),
                 'volume': self._player.volume(),
                 'volume_max': 100,
diff --git a/platypush/plugins/media/search/__init__.py b/platypush/plugins/media/search/__init__.py
index e72695c2..25ca14f2 100644
--- a/platypush/plugins/media/search/__init__.py
+++ b/platypush/plugins/media/search/__init__.py
@@ -1,5 +1,6 @@
 import logging
 
+
 class MediaSearcher:
     """
     Base class for media searchers
@@ -8,7 +9,6 @@ class MediaSearcher:
     def __init__(self, *args, **kwargs):
         self.logger = logging.getLogger(self.__class__.__name__)
 
-
     def search(self, query, *args, **kwargs):
         raise NotImplementedError('The search method should be implemented ' +
                                   'by a derived class')
diff --git a/platypush/plugins/media/search/local.py b/platypush/plugins/media/search/local.py
index f3530a81..d79bbd7c 100644
--- a/platypush/plugins/media/search/local.py
+++ b/platypush/plugins/media/search/local.py
@@ -215,11 +215,12 @@ class LocalMediaSearcher(MediaSearcher):
                     filter(MediaToken.token.in_(query_tokens)). \
                     group_by(MediaFile.path). \
                     having(func.count(MediaFileToken.token_id) >= len(query_tokens)):
-                results[file_record.path] = {
-                    'url': 'file://' + file_record.path,
-                    'title': os.path.basename(file_record.path),
-                    'size': os.path.getsize(file_record.path)
-                }
+                if (os.path.isfile(file_record.path)):
+                    results[file_record.path] = {
+                        'url': 'file://' + file_record.path,
+                        'title': os.path.basename(file_record.path),
+                        'size': os.path.getsize(file_record.path)
+                    }
 
         return results.values()
 
diff --git a/platypush/plugins/media/search/youtube.py b/platypush/plugins/media/search/youtube.py
index 2f406839..18c5f9d2 100644
--- a/platypush/plugins/media/search/youtube.py
+++ b/platypush/plugins/media/search/youtube.py
@@ -1,11 +1,12 @@
 import re
-import urllib
+import urllib.parse
+import urllib.request
 
 from platypush.context import get_plugin
 from platypush.plugins.media.search import MediaSearcher
 
 class YoutubeMediaSearcher(MediaSearcher):
-    def search(self, query):
+    def search(self, query, **kwargs):
         """
         Performs a YouTube search either using the YouTube API (faster and
         recommended, it requires the :mod:`platypush.plugins.google.youtube`
@@ -23,11 +24,12 @@ class YoutubeMediaSearcher(MediaSearcher):
 
             return self._youtube_search_html_parse(query=query)
 
-    def _youtube_search_api(self, query):
+    @staticmethod
+    def _youtube_search_api(query):
         return [
             {
                 '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
             if item.get('id', {}).get('kind') == 'youtube#video'
@@ -53,7 +55,7 @@ class YoutubeMediaSearcher(MediaSearcher):
                 html = ''
 
         self.logger.info('{} YouTube video results for the search query "{}"'
-                     .format(len(results), query))
+                         .format(len(results), query))
 
         return results
 
diff --git a/platypush/plugins/media/vlc.py b/platypush/plugins/media/vlc.py
index eabcf1ad..9043c2b7 100644
--- a/platypush/plugins/media/vlc.py
+++ b/platypush/plugins/media/vlc.py
@@ -45,6 +45,8 @@ class MediaVlcPlugin(MediaPlugin):
         self._default_fullscreen = fullscreen
         self._default_volume = volume
         self._on_stop_callbacks = []
+        self._title = None
+        self._filename = None
 
     @classmethod
     def _watched_event_types(cls):
@@ -76,6 +78,9 @@ class MediaVlcPlugin(MediaPlugin):
 
     def _reset_state(self):
         self._latest_seek = None
+        self._title = None
+        self._filename = None
+
         if self._player:
             self._player.release()
             self._player = None
@@ -105,8 +110,12 @@ class MediaVlcPlugin(MediaPlugin):
                 for cbk in self._on_stop_callbacks:
                     cbk()
             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)
             elif event.type == EventType.MediaPlayerMediaChanged:
+                self._filename = event.u.filename
+                self._title = event.u.new_title
                 self._post_event(NewPlayingMediaEvent, resource=event.u.filename)
             elif event.type == EventType.MediaPlayerLengthChanged:
                 self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource())
@@ -396,8 +405,8 @@ class MediaVlcPlugin(MediaPlugin):
         status['path'] = status['url']
         status['pause'] = status['state'] == PlayerState.PAUSE.value
         status['percent_pos'] = self._player.get_position()*100
-        status['filename'] = urllib.parse.unquote(status['url']).split('/')[-1]
-        status['title'] = status['filename']
+        status['filename'] = self._filename
+        status['title'] = self._title
         status['volume'] = self._player.audio_get_volume()
         status['volume_max'] = 100