提交 1f397005 authored 作者: Mathieu Parent's avatar Mathieu Parent

MediaBrotha: various enhancements

- add a HTML frontend
- frontend choosen automaticaly depending on browser
- handling of parent media
- max length for metadata and display_name
上级 b6593144
...@@ -35,7 +35,11 @@ require_once('MediaBrotha/Core.php'); ...@@ -35,7 +35,11 @@ require_once('MediaBrotha/Core.php');
// Init // Init
MediaBrotha_Core::init(); MediaBrotha_Core::init();
MediaBrotha_Core::connectFrontend('CiscoXML', Array('push_url' => 'http://cisco:cisco@192.168.0.220/CGI/Execute')); if (CiscoIPPhone::userAgentIsCiscoIPPhone()) {
MediaBrotha_Core::connectFrontend('CiscoXML', Array('push_url' => 'http://cisco:cisco@192.168.0.220/CGI/Execute'));
} else {
MediaBrotha_Core::connectFrontend('HTML');
}
// Backends // Backends
MediaBrotha_Core::loadBackend('FileSystem', Array('base_path' => '/home/share/music/')); MediaBrotha_Core::loadBackend('FileSystem', Array('base_path' => '/home/share/music/'));
...@@ -51,10 +55,9 @@ $ldap_connect_config = Array( ...@@ -51,10 +55,9 @@ $ldap_connect_config = Array(
MediaBrotha_Core::loadBackend('LDAP', Array('ldap_connect_config' => $ldap_connect_config)); MediaBrotha_Core::loadBackend('LDAP', Array('ldap_connect_config' => $ldap_connect_config));
// Roots // Roots
MediaBrotha_Core::addRootMedia('file:///home/share/music/', Array('name'=> 'Fichiers')); MediaBrotha_Core::addRootMedia('file:///home/share/music/', Array('display_name'=> 'Fichiers'));
MediaBrotha_Core::addRootMedia('ldap:///', Array('name'=> 'Annuaire')); MediaBrotha_Core::addRootMedia('ldap:///', Array('display_name'=> 'Annuaire'));
MediaBrotha_Core::go(); MediaBrotha_Core::go();
...@@ -23,6 +23,12 @@ This file is part of XML services for Cisco IP Phones. ...@@ -23,6 +23,12 @@ This file is part of XML services for Cisco IP Phones.
/** /**
* @author Mathieu Parent * @author Mathieu Parent
*/ */
class CiscoIPPhone {
static public function userAgentIsCiscoIPPhone() {
return preg_match('/^Allegro-Software-WebClient\//', $_SERVER['HTTP_USER_AGENT']);
}
}
class CiscoXMLField { class CiscoXMLField {
private $_allowed_fields = NULL; private $_allowed_fields = NULL;
......
...@@ -71,12 +71,17 @@ class MediaBrotha_Backend { ...@@ -71,12 +71,17 @@ class MediaBrotha_Backend {
} }
// Actions // Actions
public function getMediaActions($uri = NULL, $mime_type = NULL, $mime_encoding = NULL) { public function getMediaActions(MediaBrotha_Media $media) {
return Array(); return Array();
} }
public function doMediaAction($action, MediaBrotha_Media $media) { public function doMediaAction($action, MediaBrotha_Media $media) {
return false; return false;
} }
// Populate
public function populateMedia(MediaBrotha_Media $media) {
}
} }
...@@ -40,10 +40,13 @@ class MediaBrotha_Backend_FileSystem extends MediaBrotha_Backend { ...@@ -40,10 +40,13 @@ class MediaBrotha_Backend_FileSystem extends MediaBrotha_Backend {
// Browsing // Browsing
public function mediaFromBufferItem($file) { public function mediaFromBufferItem($file) {
if ($file) { if ($file) {
if ($file->getFilename() === '..') {
return NULL;
}
$media = new MediaBrotha_Media( $media = new MediaBrotha_Media(
'file://'.realpath($file->getPathname()), 'file://'.realpath($file->getPathname()),
Array( Array(
'name' => $file->getFilename(), 'display_name' => $file->getFilename(),
'hidden' => !$file->isReadable() || preg_match('/^\.([^.]|$)/', $file->getFilename()), 'hidden' => !$file->isReadable() || preg_match('/^\.([^.]|$)/', $file->getFilename()),
) )
); );
...@@ -59,31 +62,47 @@ class MediaBrotha_Backend_FileSystem extends MediaBrotha_Backend { ...@@ -59,31 +62,47 @@ class MediaBrotha_Backend_FileSystem extends MediaBrotha_Backend {
} }
public function fetch(MediaBrotha_Media $media) { public function fetch(MediaBrotha_Media $media) {
$uri =$media->getURI(); $uri = $media->getURI();
if ((parse_url($uri, PHP_URL_SCHEME) != 'file') || !$this->isURISafe($uri)) { if ((parse_url($uri, PHP_URL_SCHEME) != 'file') || !$this->isURISafe($uri)) {
$uri = 'file://'.$this->getMetadata('base_path'); $uri = 'file://'.$this->getParam('base_path');
} }
$path = parse_url($uri, PHP_URL_PATH); $path = parse_url($uri, PHP_URL_PATH);
if (is_dir($path)) { if (is_dir($path)) {
return new MediaBrotha_MediaIterator($this, $media, new DirectoryIterator($path)); return new MediaBrotha_MediaIterator($this, $media, new DirectoryIterator($path));
} else { } else {
$this->setURI(NULL);
return false; return false;
} }
} }
// Actions // Actions
public function getMediaActions($uri = NULL, $mime_type = NULL, $mime_encoding = NULL) { protected function _isHandled(MediaBrotha_Media $media) {
if ($mime_type === 'application/x-directory') { return ($media->getMimeType() === 'application/x-directory')
return Array( || (substr($media->getURI(), 0, 7) === 'file://');
'browse', }
);
} elseif ($uri && (substr($uri, 0, 7) === 'file://')) { public function getMediaActions(MediaBrotha_Media $media) {
if ($this->_isHandled($media)) {
return Array( return Array(
'browse', 'browse',
); );
} else { } else {
return parent::getMediaActions($uri, $mime_type, $mime_encoding); return parent::getMediaActions($media);
}
}
// Populate
public function populateMedia(MediaBrotha_Media $media) {
if ($this->_isHandled($media)) {
$parent_path = 'file://'.dirname(parse_url($media->getURI(), PHP_URL_PATH));
if ($this->isURISafe($parent_path)) {
$media->setParent(
new MediaBrotha_Media($parent_path,
Array(
'display_name' => '..',
)
)
);
}
} }
} }
......
...@@ -54,7 +54,7 @@ class MediaBrotha_Backend_LDAP extends MediaBrotha_Backend { ...@@ -54,7 +54,7 @@ class MediaBrotha_Backend_LDAP extends MediaBrotha_Backend {
$media = new MediaBrotha_Media( $media = new MediaBrotha_Media(
'ldap:///'.$entry->dn(), 'ldap:///'.$entry->dn(),
Array( Array(
'name' => $entry->dn(), //$entry->getValue('sn', 'single'), 'display_name' => $entry->dn(), //$entry->getValue('sn', 'single'),
), ),
'text/directory' 'text/directory'
//$finfo->file($file->getPathname(), FILEINFO_MIME_ENCODING); //$finfo->file($file->getPathname(), FILEINFO_MIME_ENCODING);
...@@ -79,17 +79,18 @@ class MediaBrotha_Backend_LDAP extends MediaBrotha_Backend { ...@@ -79,17 +79,18 @@ class MediaBrotha_Backend_LDAP extends MediaBrotha_Backend {
return true; return true;
} }
// Actions // Actions
public function getMediaActions($uri = NULL, $mime_type = NULL, $mime_encoding = NULL) { protected function _isHandled(MediaBrotha_Media $media) {
if ($mime_type === 'text/directory') { return ($media->getMimeType() === 'text/directory')
return Array( || (substr($media->getURI(), 0, 7) === 'ldap://');
'browse', }
);
} elseif ($uri && (substr($uri, 0, 7) === 'ldap://')) { public function getMediaActions(MediaBrotha_Media $media) {
if ($this->_isHandled($media)) {
return Array( return Array(
'browse', 'browse',
); );
} else { } else {
return parent::getMediaActions($uri, $mime_type, $mime_encoding); return parent::getMediaActions($media);
} }
} }
......
...@@ -30,11 +30,18 @@ class MediaBrotha_Backend_Root extends MediaBrotha_Backend { ...@@ -30,11 +30,18 @@ class MediaBrotha_Backend_Root extends MediaBrotha_Backend {
public function __construct($params = NULL) { public function __construct($params = NULL) {
parent::__construct($params); parent::__construct($params);
$this->_media = new MediaBrotha_Media('MediaBrotha:///'); $this->_media = new MediaBrotha_Media('MediaBrotha:///',
Array(
'display_name' => 'Root',
)
);
$this->_media_iterator = new MediaBrotha_MediaIterator($this, $this->_media); $this->_media_iterator = new MediaBrotha_MediaIterator($this, $this->_media);
} }
public function addRootMedia($media) { public function addRootMedia($URI, array $metadata = Array(), $mime_type = NULL, $mime_encoding = NULL) {
$this->addRootMediaObj(new MediaBrotha_Media($URI, $metadata, $mime_type,$mime_encoding));
}
public function addRootMediaObj($media) {
$this->_media_iterator->bufferAdd($media); $this->_media_iterator->bufferAdd($media);
} }
...@@ -42,13 +49,6 @@ class MediaBrotha_Backend_Root extends MediaBrotha_Backend { ...@@ -42,13 +49,6 @@ class MediaBrotha_Backend_Root extends MediaBrotha_Backend {
public function fetch(MediaBrotha_Media $media) { public function fetch(MediaBrotha_Media $media) {
return $this->_media_iterator; return $this->_media_iterator;
} }
// Actions
public function getMediaActions($uri = NULL, $mime_type = NULL, $mime_encoding = NULL) {
return Array(
);
}
} }
...@@ -50,17 +50,13 @@ class MediaBrotha_Backend_VLC extends MediaBrotha_Backend { ...@@ -50,17 +50,13 @@ class MediaBrotha_Backend_VLC extends MediaBrotha_Backend {
} }
// Actions // Actions
public function getMediaActions($uri = NULL, $mime_type = NULL, $mime_encoding = NULL) { protected function _isHandled(MediaBrotha_Media $media) {
if (in_array($mime_type, Array($this::$_supported_mime_types))) { return in_array($media->getMimeType(), Array($this::$_supported_mime_types))
return Array( || (substr($media->getURI(), 0, 7) === 'file://');
'play_media', }
'pause_media',
'stop_media', public function getMediaActions(MediaBrotha_Media $media) {
'enqueue_media', if ($this->_isHandled($media)) {
'next_media',
'previous_media',
);
} elseif ($uri && (substr($uri, 0, 7) === 'file://')) {
return Array( return Array(
'play_media', 'play_media',
'pause_media', 'pause_media',
...@@ -70,7 +66,7 @@ class MediaBrotha_Backend_VLC extends MediaBrotha_Backend { ...@@ -70,7 +66,7 @@ class MediaBrotha_Backend_VLC extends MediaBrotha_Backend {
'previous_media', 'previous_media',
); );
} else { } else {
return parent::getMediaActions($uri, $mime_type, $mime_encoding); return parent::getMediaActions($media);
} }
} }
public function doMediaAction($action, MediaBrotha_Media $media) { public function doMediaAction($action, MediaBrotha_Media $media) {
......
...@@ -34,47 +34,17 @@ class MediaBrotha_Core { ...@@ -34,47 +34,17 @@ class MediaBrotha_Core {
private static $_backends = Array(); private static $_backends = Array();
private static $_media = NULL; private static $_media = NULL;
/*
* Core
*/
public static function init() { public static function init() {
if (!session_start()) { if (!session_start()) {
die('Unable to start session'); die('Unable to start session');
} }
if (!isset($_SESSION['HASHES'])) { MediaBrotha_Core::_unhashRequest();
$_SESSION['HASHES'] = Array();
}
if (isset($_SERVER["QUERY_STRING"])) {
$tmp = explode('&', $_SERVER["QUERY_STRING"]);
foreach ($tmp as $h) {
if (preg_match('/^[a-z0-9]+$/', $h)) {
if ($s = MediaBrotha_Core::hash2string($h)) {
parse_str($s, $output);
$_GET = array_merge($_GET, $output);
}
}
}
}
MediaBrotha_Core::loadBackend('Root'); MediaBrotha_Core::loadBackend('Root');
} }
public static function hash2string($h) {
if (isset($_SESSION['HASHES'][$h])) {
return $_SESSION['HASHES'][$h];
} else {
//print 'NO string for hash '.$h;
return NULL;
}
}
public static function string2hash($s) {
for($hash_length = 1 ; $hash_length <=41 ; $hash_length++) {
$h = substr(sha1($s), 0, $hash_length);
if (!isset($_SESSION['HASHES'][$h]) || ($_SESSION['HASHES'][$h] === $s)) {
break;
}
}
$_SESSION['HASHES'][$h] = $s;
return $h;
}
public static function go() { public static function go() {
$current_backend = MediaBrotha_Core::getCurrentBackend(); $current_backend = MediaBrotha_Core::getCurrentBackend();
$current_media = MediaBrotha_Core::getCurrentMedia(); $current_media = MediaBrotha_Core::getCurrentMedia();
...@@ -82,7 +52,7 @@ class MediaBrotha_Core { ...@@ -82,7 +52,7 @@ class MediaBrotha_Core {
if ($media_iterator = $current_backend->fetch($current_media)) { if ($media_iterator = $current_backend->fetch($current_media)) {
MediaBrotha_Core::$_frontend->begin($current_media); MediaBrotha_Core::$_frontend->begin($current_media);
foreach ($media_iterator as $item) { foreach ($media_iterator as $item) {
if ($item->isHidden()) { if (($item === NULL) || $item->isHidden()) {
continue; continue;
} }
MediaBrotha_Core::$_frontend->addItem($item); MediaBrotha_Core::$_frontend->addItem($item);
...@@ -97,10 +67,9 @@ class MediaBrotha_Core { ...@@ -97,10 +67,9 @@ class MediaBrotha_Core {
} }
public function addRootMedia($URI, array $metadata = Array(), $mime_type = NULL, $mime_encoding = NULL) { public function addRootMedia($URI, array $metadata = Array(), $mime_type = NULL, $mime_encoding = NULL) {
MediaBrotha_Core::$_backends['Root']->addRootMedia( MediaBrotha_Core::$_backends['Root']->addRootMedia($URI, $metadata, $mime_type, $mime_encoding);
new MediaBrotha_Media($URI, $metadata, $mime_type,$mime_encoding)
);
} }
/* /*
* Backends * Backends
*/ */
...@@ -120,22 +89,7 @@ class MediaBrotha_Core { ...@@ -120,22 +89,7 @@ class MediaBrotha_Core {
public static function getBackends() { public static function getBackends() {
return MediaBrotha_Core::$_backends; return MediaBrotha_Core::$_backends;
} }
public static function getCurrentBackend() {
if (!empty($_GET['backend']) && !empty(MediaBrotha_Core::$_backends[$_GET['backend']])) {
return MediaBrotha_Core::$_backends[$_GET['backend']];
}
foreach (MediaBrotha_Core::getBackends() as $backend) {
foreach ($backend->getMediaActions(MediaBrotha_Core::getCurrentURI()) as $action) {
if ($backend->getBackendName() == 'Root') {
continue;
}
if ($action === MediaBrotha_Core::getAction()) {
return $backend;
}
}
}
return MediaBrotha_Core::$_backends['Root'];
}
/* /*
* Frontend * Frontend
*/ */
...@@ -151,8 +105,26 @@ class MediaBrotha_Core { ...@@ -151,8 +105,26 @@ class MediaBrotha_Core {
MediaBrotha_Core::$_frontend = new $class_name($args); MediaBrotha_Core::$_frontend = new $class_name($args);
} }
/* /*
* ... * Current *
*/ */
public static function getCurrentBackend() {
if (!empty($_GET['backend']) && !empty(MediaBrotha_Core::$_backends[$_GET['backend']])) {
return MediaBrotha_Core::$_backends[$_GET['backend']];
}
$current_media = MediaBrotha_Core::getCurrentMedia();
foreach (MediaBrotha_Core::getBackends() as $backend) {
foreach ($backend->getMediaActions($current_media) as $action) {
if ($backend->getBackendName() == 'Root') {
continue;
}
if ($action === MediaBrotha_Core::getAction()) {
return $backend;
}
}
}
return MediaBrotha_Core::$_backends['Root'];
}
public static function getAction() { public static function getAction() {
if (!empty($_GET['action'])) { if (!empty($_GET['action'])) {
return $_GET['action']; return $_GET['action'];
...@@ -161,17 +133,63 @@ class MediaBrotha_Core { ...@@ -161,17 +133,63 @@ class MediaBrotha_Core {
} }
} }
public static function getCurrentURI() { public static function getCurrentMedia() {
if (empty(MediaBrotha_Core::$_media)) {
if (!empty($_GET['uri'])) { if (!empty($_GET['uri'])) {
return (string) $_GET['uri']; MediaBrotha_Core::$_media = new MediaBrotha_Media($_GET['uri']);
} else {
MediaBrotha_Core::$_media = new MediaBrotha_Media('MediaBrotha:///');
} }
foreach (MediaBrotha_Core::getBackends() as $backend) {
$backend->populateMedia(MediaBrotha_Core::$_media);
} }
public static function getCurrentMedia() {
if (empty(MediaBrotha_Core::$_media)) {
MediaBrotha_Core::$_media = new MediaBrotha_Media(MediaBrotha_Core::getCurrentURI());
} }
return MediaBrotha_Core::$_media; return MediaBrotha_Core::$_media;
} }
/*
* helper functions
*/
/* hashes */
public static function hash2value($h) {
if (isset($_SESSION['HASHES'][$h])) {
return $_SESSION['HASHES'][$h];
} else {
return NULL;
}
}
public static function value2hash($v) {
for($hash_length = 1 ; $hash_length <=41 ; $hash_length++) {
$h = substr(sha1($v), 0, $hash_length);
if (!isset($_SESSION['HASHES'][$h]) || ($_SESSION['HASHES'][$h] === $v)) {
break;
}
}
$_SESSION['HASHES'][$h] = $v;
return $h;
}
public static function hash2array($h) {
if ($v = MediaBrotha_Core::hash2value($h)) {
parse_str($v, $output);
return $output;
}
}
private static function _unhashRequest() {
if (!isset($_SESSION['HASHES'])) {
$_SESSION['HASHES'] = Array();
}
if (isset($_SERVER["QUERY_STRING"])) {
$tmp = explode('&', $_SERVER["QUERY_STRING"]);
foreach ($tmp as $h) {
if (preg_match('/^[a-z0-9]+$/', $h)) {
$_GET = array_merge($_GET, MediaBrotha_Core::hash2array($h));
}
}
}
}
} }
...@@ -30,12 +30,15 @@ class MediaBrotha_Frontend_CiscoXML extends MediaBrotha_Frontend_HTTP { ...@@ -30,12 +30,15 @@ class MediaBrotha_Frontend_CiscoXML extends MediaBrotha_Frontend_HTTP {
private $_xml = NULL; private $_xml = NULL;
public function begin($item) { public function begin($item) {
$this->_xml = new CiscoIPPhoneMenu(); $this->_xml = new CiscoIPPhoneMenu();
$this->_xml->setCiscoElement('Title', $item->getDisplayName()); $this->_xml->setCiscoElement('Title', $item->getDisplayName(32));
$this->_xml->setCiscoElement('Prompt', substr($item->getURI(), -32)); $this->_xml->setCiscoElement('Prompt', substr($item->getURI(), -32));
if ($parent = $item->getParent()) {
$this->addItem($parent);
}
} }
public function addItem($item) { public function addItem($item) {
$url = $this->rootURL().'?'. $url = $this->rootURL().'?'.
MediaBrotha_Core::string2hash( MediaBrotha_Core::value2hash(
'mime_type='.urlencode($item->getMimeType()).'&'. 'mime_type='.urlencode($item->getMimeType()).'&'.
($item->getMimeEncoding() ? ('mime_encoding='.urlencode($item->getMimeEncoding()).'&') : ''). ($item->getMimeEncoding() ? ('mime_encoding='.urlencode($item->getMimeEncoding()).'&') : '').
'uri='.urlencode($item->getURI())); 'uri='.urlencode($item->getURI()));
...@@ -49,10 +52,10 @@ class MediaBrotha_Frontend_CiscoXML extends MediaBrotha_Frontend_HTTP { ...@@ -49,10 +52,10 @@ class MediaBrotha_Frontend_CiscoXML extends MediaBrotha_Frontend_HTTP {
'URL' => 'SoftKey:Select', 'URL' => 'SoftKey:Select',
'Position' => $pos++)); 'Position' => $pos++));
foreach (MediaBrotha_Core::getBackends() as $backend) { foreach (MediaBrotha_Core::getBackends() as $backend) {
foreach ($backend->getMediaActions(MediaBrotha_Core::getCurrentURI()) as $action) { foreach ($backend->getMediaActions(MediaBrotha_Core::getCurrentMedia()) as $action) {
$this->_xml->setCiscoElement('SoftKeyItem', $this->_xml->setCiscoElement('SoftKeyItem',
Array('Name' => $action, Array('Name' => $action,
'URL' => 'QueryStringParam:'.MediaBrotha_Core::string2hash('action='.$action.'&backend='.$backend->getBackendName()), 'URL' => 'QueryStringParam:'.MediaBrotha_Core::value2hash('action='.$action.'&backend='.$backend->getBackendName()),
'Position' => $pos++)); 'Position' => $pos++));
} }
} }
......
...@@ -25,14 +25,15 @@ This file is part of MediaBrotha. ...@@ -25,14 +25,15 @@ This file is part of MediaBrotha.
*/ */
class MediaBrotha_Media { class MediaBrotha_Media {
private $_metadata = Array();
private $_URI = NULL; private $_URI = NULL;
private $_metadata = Array();
private $_mimeType = NULL; private $_mimeType = NULL;
private $_mimeEncoding = NULL; private $_mimeEncoding = NULL;
private $_parent = NULL;
public function __construct($URI, array $metadata = Array(), $mime_type = NULL, $mime_encoding = NULL) { public function __construct($URI, array $metadata = Array(), $mime_type = NULL, $mime_encoding = NULL) {
$this->setURI($URI); $this->_URI = $URI;
$this->setMetadata($metadata); $this->setMetadata($metadata);
$this->setMimeType($mime_type); $this->setMimeType($mime_type);
$this->setMimeEncoding($mime_encoding); $this->setMimeEncoding($mime_encoding);
...@@ -42,16 +43,17 @@ class MediaBrotha_Media { ...@@ -42,16 +43,17 @@ class MediaBrotha_Media {
public function getURI() { public function getURI() {
return $this->_URI; return $this->_URI;
} }
public function setURI($URI) {
$this->_URI = $URI;
}
/* metadata */ /* metadata */
public function getMetadata($name = NULL) { public function getMetadata($name = NULL, $max_length = NULL) {
if ($name === NULL) { if ($name === NULL) {
return $this->_metadata; return $this->_metadata;
} elseif (isset($this->_metadata[$name])) { } elseif (isset($this->_metadata[$name])) {
if ($max_length) {
return substr($this->_metadata[$name], 0, $max_length);
} else {
return $this->_metadata[$name]; return $this->_metadata[$name];
}
} else { } else {
return NULL; return NULL;
} }
...@@ -60,6 +62,17 @@ class MediaBrotha_Media { ...@@ -60,6 +62,17 @@ class MediaBrotha_Media {
if (is_array($name)) { if (is_array($name)) {
$this->_metadata = $name; $this->_metadata = $name;
} else { } else {
switch($name) {
case 'hidden':
case 'display_name':
case 'Array':
//music
case 'artist':
case 'album':
break;
default:
print "Unusual metadata '$name'.\n";
}
$this->_metadata[$name] = $value; $this->_metadata[$name] = $value;
} }
...@@ -81,10 +94,22 @@ class MediaBrotha_Media { ...@@ -81,10 +94,22 @@ class MediaBrotha_Media {
$this->_mimeEncoding = $mime_encoding; $this->_mimeEncoding = $mime_encoding;
} }
/* parent */
public function getParent() {
return $this->_parent;
}
public function setParent($parent) {
$this->_parent = $parent;
}
/* Most used metadata */ /* Most used metadata */
public function getDisplayName($max_length = 0) { public function getDisplayName($max_length = NULL) {
if ($name = $this->getMetadata('name')) { if ($name = $this->getMetadata('display_name')) {
if ($max_length) {
return substr($name, 0, $max_length);
} else {
return $name; return $name;
}
} else { } else {
return '?'; return '?';
} }
...@@ -97,13 +122,3 @@ class MediaBrotha_Media { ...@@ -97,13 +122,3 @@ class MediaBrotha_Media {
} }
} }
/*
Mandatory metadata:
uri
name
Usual metadata:
artist
album
*/
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论