Merge branch 'cc-5709-airtime-analyzer' into cc-5709-airtime-analyzer-saas

Conflicts:
	airtime_mvc/locale/cs_CZ/LC_MESSAGES/airtime.po
	airtime_mvc/locale/de_AT/LC_MESSAGES/airtime.po
	airtime_mvc/locale/de_DE/LC_MESSAGES/airtime.po
	airtime_mvc/locale/el_GR/LC_MESSAGES/airtime.po
	airtime_mvc/locale/en_CA/LC_MESSAGES/airtime.po
	airtime_mvc/locale/en_GB/LC_MESSAGES/airtime.po
	airtime_mvc/locale/en_US/LC_MESSAGES/airtime.po
	airtime_mvc/locale/es_ES/LC_MESSAGES/airtime.po
	airtime_mvc/locale/fr_FR/LC_MESSAGES/airtime.po
	airtime_mvc/locale/hr_HR/LC_MESSAGES/airtime.po
	airtime_mvc/locale/hu_HU/LC_MESSAGES/airtime.po
	airtime_mvc/locale/it_IT/LC_MESSAGES/airtime.po
	airtime_mvc/locale/ko_KR/LC_MESSAGES/airtime.po
	airtime_mvc/locale/nl_NL/LC_MESSAGES/airtime.po
	airtime_mvc/locale/pl_PL/LC_MESSAGES/airtime.po
	airtime_mvc/locale/pt_BR/LC_MESSAGES/airtime.po
	airtime_mvc/locale/ru_RU/LC_MESSAGES/airtime.po
	airtime_mvc/locale/sr_RS/LC_MESSAGES/airtime.po
	airtime_mvc/locale/sr_RS@latin/LC_MESSAGES/airtime.po
	airtime_mvc/locale/template/airtime.po
	airtime_mvc/locale/zh_CN/LC_MESSAGES/airtime.po
This commit is contained in:
Albert Santoni 2014-04-25 00:43:22 -04:00
commit 446eca057c
20 changed files with 257 additions and 71 deletions

View File

@ -16,6 +16,7 @@ require_once "Database.php";
require_once "Timezone.php";
require_once __DIR__.'/forms/helpers/ValidationTypes.php';
require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php';
require_once __DIR__.'/controllers/plugins/Maintenance.php';
require_once (APPLICATION_PATH."/logging/Logging.php");
Logging::setLogPath('/var/log/airtime/zendphp.log');
@ -198,5 +199,11 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
'action' => 'password-change',
)));
}
public function _initPlugins()
{
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Zend_Controller_Plugin_Maintenance());
}
}

View File

@ -18,4 +18,9 @@ class IndexController extends Zend_Controller_Action
$this->_helper->layout->setLayout('layout');
}
public function maintenanceAction()
{
$this->getResponse()->setHttpResponseCode(503);
}
}

View File

@ -17,6 +17,14 @@ class UpgradeController extends Zend_Controller_Action
return;
}
//Disable Airtime UI
//create a temporary maintenance notification file
//when this file is on the server, zend framework redirects all
//requests to the maintenance page and sets a 503 response code
$maintenanceFile = '/tmp/maintenance.txt';
$file = fopen($maintenanceFile, 'w');
fclose($file);
//Begin upgrade
$filename = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
$values = parse_ini_file($filename, true);
@ -69,6 +77,46 @@ class UpgradeController extends Zend_Controller_Action
$file = new SplFileObject($iniFile, "w");
$file->fwrite($beginning."\n".$newLines.$end);
$iniFile = isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."application.ini" : "/usr/share/airtime/application/configs/application.ini";
//update application.ini
$newLines = "resources.frontController.moduleDirectory = APPLICATION_PATH '/modules'\n".
"resources.frontController.plugins.putHandler = 'Zend_Controller_Plugin_PutHandler'\n".
";load everything in the modules directory including models\n".
"resources.modules[] = ''\n";
$currentIniFile = file_get_contents($iniFile);
/* We want to add the new lines immediately after the first line, '[production]'
* We read the first line into $beginning, and the rest of the file into $end.
* Then overwrite the current application.ini file with $beginning, $newLines, and $end
*/
$lines = explode("\n", $currentIniFile);
$beginning = implode("\n", array_slice($lines, 0,1));
//check that first line is '[production]'
if ($beginning != '[production]') {
$this->getResponse()
->setHttpResponseCode(400)
->appendBody('Upgrade to Airtime 2.5.3 FAILED. Could not upgrade application.ini');
return;
}
$end = implode("\n", array_slice($lines, 1));
if (!is_writeable($iniFile)) {
$this->getResponse()
->setHttpResponseCode(400)
->appendBody('Upgrade to Airtime 2.5.3 FAILED. Could not upgrade application.ini');
return;
}
$file = new SplFileObject($iniFile, "w");
$file->fwrite($beginning."\n".$newLines.$end);
//delete maintenance.txt to give users access back to Airtime
unlink($maintenanceFile);
//TODO: clear out the cache
$this->getResponse()
->setHttpResponseCode(200)
->appendBody("Upgrade to Airtime 2.5.3 OK");
@ -104,12 +152,12 @@ class UpgradeController extends Zend_Controller_Action
->findOne();
$airtime_version = $pref->getValStr();
if ($airtime_version != '2.5.2') {
if ($airtime_version != '2.5.2' || $airtime_version != '2.5.1') {
$this->getResponse()
->setHttpResponseCode(400)
->appendBody("Upgrade to Airtime 2.5.3 FAILED. You must be using Airtime 2.5.2 to upgrade.");
->appendBody("Upgrade to Airtime 2.5.3 FAILED. You must be using Airtime 2.5.1 or 2.5.2 to upgrade.");
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,15 @@
<?php
class Zend_Controller_Plugin_Maintenance extends Zend_Controller_Plugin_Abstract
{
protected $maintenanceFile = '/tmp/maintenance.txt';
public function preDispatch(Zend_Controller_Request_Abstract $request) {
if (file_exists($this->maintenanceFile)) {
$request->setModuleName('default')
->setControllerName('index')
->setActionName('maintenance')
->setDispatched(true);
}
}
}

View File

@ -641,6 +641,11 @@ class Application_Model_Preference
{
return self::getValue("logoImage");
}
public static function SetUniqueId($id)
{
self::setValue("uniqueId", $id);
}
public static function GetUniqueId()
{
@ -890,6 +895,11 @@ class Application_Model_Preference
return self::getValue("enable_stream_conf");
}
public static function SetAirtimeVersion($version)
{
self::setValue("system_version", $version);
}
public static function GetAirtimeVersion()
{

View File

@ -0,0 +1 @@
Airtime is down for maintenance. We'll be back soon!

View File

@ -330,16 +330,28 @@ INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('en_GB', 'English (Brit
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('en_US', 'English (USA)');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('cs_CZ', 'Český');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('de_DE', 'Deutsch');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('de_AT', 'Österreichisches Deutsch');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('hu_HU', 'Magyar');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('de_AT', 'Deutsch (Österreich)');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('el_GR', 'Ελληνικά');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('es_ES', 'Español');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('fr_FR', 'Français');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('hr_HR', 'Hrvatski');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('hu_HU', 'Magyar');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('it_IT', 'Italiano');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('ko_KR', '한국어');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('pl_PL', 'Polski');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('pt_BR', 'Português Brasileiro');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('pt_BR', 'Português (Brasil)');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('ru_RU', 'Русский');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('sr_RS', 'Српски (Ћирилица)');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('sr_RS@latin', 'Srpski (Latinica)');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('zh_CN', '简体中文');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('el_GR', 'Ελληνικά');
-- end of added in 2.3
-- added in 2.5.2
INSERT INTO cc_pref (keystr, valstr) VALUES ('timezone', 'UTC');
INSERT INTO cc_pref (subjid, keystr, valstr) VALUES (1, 'user_timezone', 'UTC');
INSERT INTO cc_pref (keystr, valstr) VALUES ('import_timestamp', '0');
--end added in 2.5.2

View File

@ -0,0 +1,19 @@
//Croatian
{
"sProcessing": "Procesiram...",
"sLengthMenu": "Prikaži _MENU_ rezultata po stranici",
"sZeroRecords": "Ništa nije pronađeno",
"sInfo": "Prikazano _START_ do _END_ od _TOTAL_ rezultata",
"sInfoEmpty": "Prikazano 0 do 0 od 0 rezultata",
"sInfoFiltered": "(filtrirano iz _MAX_ ukupnih rezultata)",
"sInfoPostFix": "",
"sSearch": "Filter",
"sUrl": "",
"oPaginate": {
"sFirst": "Prva",
"sPrevious": "Nazad",
"sNext": "Naprijed",
"sLast": "Zadnja"
}
}

View File

@ -0,0 +1,19 @@
//Serbian Cyrillic
{
"sProcessing": "Обрада...",
"sLengthMenu": "_MENU_ Резултати по страници",
"sZeroRecords": "Ништа није пронађено",
"sInfo": "Резултати: _START_ - _END_ Укупно: _TOTAL_",
"sInfoEmpty": "Нула Резултат",
"sInfoFiltered": "(филтрирано из _MAX_ укупних резултата)",
"sInfoPostFix": "",
"sSearch": "Филтер",
"sUrl": "",
"oPaginate": {
"sFirst": "Прва",
"sPrevious": "Назад",
"sNext": "Напред",
"sLast": "Задња"
}
}

View File

@ -0,0 +1,19 @@
//Serbian Latin
{
"sProcessing": "Obrada...",
"sLengthMenu": "_MENU_ Rezultati po stranici",
"sZeroRecords": "Ništa nije pronađeno",
"sInfo": "Rezultati: _START_ - _END_ Ukupno: _TOTAL_",
"sInfoEmpty": "Nula Rezultat",
"sInfoFiltered": "(filtrirano iz _MAX_ ukupnih rezultata)",
"sInfoPostFix": "",
"sSearch": "Filter",
"sUrl": "",
"oPaginate": {
"sFirst": "Prva",
"sPrevious": "Nazad",
"sNext": "Napred",
"sLast": "Zadnja"
}
}

View File

@ -0,0 +1,25 @@
// Croatian
plupload.addI18n({
'Select files': 'Izaberite datoteke:',
'Add files to the upload queue and click the start button.': 'Dodajte datoteke u listu i kliknite Upload.',
'Filename': 'Ime datoteke',
'Status': 'Status',
'Size': 'Veličina',
'Add files': 'Dodajte datoteke',
'Stop current upload': 'Zaustavi trenutan upload',
'Start uploading queue': 'Pokreni Upload',
'Uploaded %d/%d files': 'Uploadano %d/%d datoteka',
'N/A': 'N/A',
'Drag files here.': 'Dovucite datoteke ovdje',
'File extension error.': 'Greška ekstenzije datoteke.',
'File size error.': 'Greška veličine datoteke.',
'Init error.': 'Greška inicijalizacije.',
'HTTP Error.': 'HTTP greška.',
'Security error.': 'Sigurnosna greška.',
'Generic error.': 'Generička greška.',
'IO error.': 'I/O greška.',
'Stop Upload': 'Zaustavi upload.',
'Add Files': 'Dodaj datoteke',
'Start Upload': 'Pokreni upload.',
'%d files queued': '%d datoteka na čekanju.'
});

View File

@ -0,0 +1,14 @@
// Serbian Cyrillic
plupload.addI18n({
'Select files' : 'Изаберите фајлове',
'Add files to the upload queue and click the start button.' : 'Додајте фајлове у листу и кликните на дугме Старт.',
'Filename' : 'Назив фајла',
'Status' : 'Status',
'Size' : 'Величина',
'Add Files' : 'Додај фајлове',
'Stop current upload' : 'Заустави upload',
'Start uploading queue' : 'Почни upload',
'Drag files here.' : 'Превуци фајлове овде.',
'Start Upload': 'Почни upload',
'Uploaded %d/%d files': 'Снимљено %d/%d фајлова'
});

View File

@ -0,0 +1,14 @@
// Serbian Latin
plupload.addI18n({
'Select files' : 'Izaberite fajlove',
'Add files to the upload queue and click the start button.' : 'Dodajte fajlove u listu i kliknite na dugme Start.',
'Filename' : 'Naziv fajla',
'Status' : 'Status',
'Size' : 'Veličina',
'Add Files' : 'Dodaj fajlove',
'Stop current upload' : 'Zaustavi upload',
'Start uploading queue' : 'Počni upload',
'Drag files here.' : 'Prevucite fajlove ovde.',
'Start Upload': 'Počni upload',
'Uploaded %d/%d files': 'Snimljeno %d/%d fajlova'
});

View File

@ -308,62 +308,15 @@ class AirtimeInstall
$sql = "DELETE FROM cc_pref WHERE keystr = 'system_version'";
$con->exec($sql);
$sql = "INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '$p_version')";
$result = $con->exec($sql);
if ($result < 1) {
return false;
}
return true;
Application_Model_Preference::SetAirtimeVersion($p_version);
}
public static function SetUniqueId()
{
$con = Propel::getConnection();
$uniqueId = md5(uniqid("", true));
$sql = "INSERT INTO cc_pref (keystr, valstr) VALUES ('uniqueId', '$uniqueId')";
$result = $con->exec($sql);
if ($result < 1) {
return false;
}
return true;
Application_Model_Preference::SetUniqueId($uniqueId);
}
public static function SetDefaultTimezone()
{
$con = Propel::getConnection();
// we need to run php as commandline because we want to get the timezone in cli php.ini file
//$defaultTimezone = exec("php -r 'echo date_default_timezone_get().PHP_EOL;'");
$defaultTimezone = exec("cat /etc/timezone");
$defaultTimezone = trim($defaultTimezone);
if((!in_array($defaultTimezone, DateTimeZone::listIdentifiers()))){
$defaultTimezone = "UTC";
}
$sql = "INSERT INTO cc_pref (keystr, valstr) VALUES ('timezone', '$defaultTimezone')";
$result = $con->exec($sql);
if ($result < 1) {
return false;
}
$sql = "INSERT INTO cc_pref (subjid, keystr, valstr) VALUES (1, 'user_timezone', '$defaultTimezone')";
$result = $con->exec($sql);
if ($result < 1) {
return false;
}
return true;
}
public static function SetImportTimestamp()
{
$con = Propel::getConnection();
$sql = "INSERT INTO cc_pref (keystr, valstr) VALUES ('import_timestamp', '0')";
$result = $con->exec($sql);
if ($result < 1) {
return false;
}
return true;
}
public static function GetAirtimeVersion()
{
$con = Propel::getConnection();

View File

@ -10,10 +10,19 @@ require_once(__DIR__.'/AirtimeInstall.php');
require_once(__DIR__.'/airtime-constants.php');
require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/conf.php');
//Propel classes.
set_include_path(AirtimeInstall::GetAirtimeSrcDir().'/application/models' . PATH_SEPARATOR . get_include_path());
$CC_CONFIG = Config::getConfig();
require_once 'propel/runtime/lib/Propel.php';
Propel::init(AirtimeInstall::GetAirtimeSrcDir()."/application/configs/airtime-conf-production.php");
//use this class to set new values in the cache as well.
require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/common/Database.php');
require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/models/Preference.php');
echo PHP_EOL."* Database Installation".PHP_EOL;
AirtimeInstall::CreateDatabaseUser();
@ -56,10 +65,8 @@ AirtimeInstall::SetAirtimeVersion(AIRTIME_VERSION);
if (AirtimeInstall::$databaseTablesCreated) {
AirtimeInstall::SetDefaultTimezone();
// set up some keys in DB
AirtimeInstall::SetUniqueId();
AirtimeInstall::SetImportTimestamp();
$ini = parse_ini_file(__DIR__."/airtime-install.ini");

View File

@ -0,0 +1,8 @@
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('hr_HR', 'Hrvatski');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('sr_RS', 'Српски (Ћирилица)');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('sr_RS@latin', 'Srpski (Latinica)');
UPDATE cc_locale SET locale_lang='Deutsch (Österreich)' WHERE locale_code='de_AT';
UPDATE cc_locale SET locale_lang='Português Brasileiro' WHERE locale_code='pt_BR';
-- NOTE BECAUSE OF CACHING NOW ANY UPGRADES TO cc_pref MUST NOT BE DONE HERE.

View File

@ -1,6 +1,7 @@
import time
import datetime
import mutagen
import logging
from analyzer import Analyzer
class MetadataAnalyzer(Analyzer):
@ -52,7 +53,7 @@ class MetadataAnalyzer(Analyzer):
try:
#Special handling for getting the # of channels from MP3s. It's in the "mode" field
#which is 0=Stereo, 1=Joint Stereo, 2=Dual Channel, 3=Mono. Part of the ID3 spec...
if metadata["mime"] == "audio/mpeg":
if metadata["mime"] in ["audio/mpeg", 'audio/mp3']:
if info.mode == 3:
metadata["channels"] = 1
else:
@ -92,6 +93,7 @@ class MetadataAnalyzer(Analyzer):
'genre': 'genre',
'isrc': 'isrc',
'label': 'label',
'organization': 'label',
#'length': 'length',
'language': 'language',
'last_modified':'last_modified',

View File

@ -45,6 +45,13 @@ def process_http_requests(ipc_queue, http_retry_queue_path):
pass
else:
raise e
except Exception as e:
# If we fail to unpickle a saved queue of failed HTTP requests, then we'll just log an error
# and continue because those HTTP requests are lost anyways. The pickled file will be
# overwritten the next time the analyzer is shut down too.
logging.error("Failed to unpickle %s. Continuing..." % http_retry_queue_path)
pass
while not shutdown:
try:
@ -91,7 +98,8 @@ def send_http_request(picklable_request, retry_queue):
logging.error("HTTP request failed with unhandled exception. %s" % str(e))
# Don't put the request into the retry queue, just give up on this one.
# I'm doing this to protect against us getting some pathological request
# that breaks our code. I don't want us having
# that breaks our code. I don't want us pickling data that potentially
# breaks airtime_analyzer.

View File

@ -28,7 +28,7 @@ def test_basic():
assert metadata['album_title'] == u'Test Album'
assert metadata['year'] == u'1999'
assert metadata['genre'] == u'Test Genre'
assert metadata['mime'] == 'audio/mpeg' # Not unicode because MIMEs aren't.
assert metadata['mime'] == 'audio/mp3' # Not unicode because MIMEs aren't.
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata["length"] == str(datetime.timedelta(seconds=metadata["length_seconds"]))
assert os.path.exists(DEFAULT_IMPORT_DEST)

View File

@ -26,7 +26,7 @@ def test_mp3_mono():
assert metadata['channels'] == 1
assert metadata['bit_rate'] == 64000
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata['mime'] == 'audio/mpeg' # Not unicode because MIMEs aren't.
assert metadata['mime'] == 'audio/mp3' # Not unicode because MIMEs aren't.
assert metadata['track_total'] == u'10' # MP3s can have a track_total
#Mutagen doesn't extract comments from mp3s it seems
@ -36,7 +36,7 @@ def test_mp3_jointstereo():
assert metadata['channels'] == 2
assert metadata['bit_rate'] == 128000
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata['mime'] == 'audio/mpeg'
assert metadata['mime'] == 'audio/mp3'
assert metadata['track_total'] == u'10' # MP3s can have a track_total
def test_mp3_simplestereo():
@ -45,7 +45,7 @@ def test_mp3_simplestereo():
assert metadata['channels'] == 2
assert metadata['bit_rate'] == 128000
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata['mime'] == 'audio/mpeg'
assert metadata['mime'] == 'audio/mp3'
assert metadata['track_total'] == u'10' # MP3s can have a track_total
def test_mp3_dualmono():
@ -54,7 +54,7 @@ def test_mp3_dualmono():
assert metadata['channels'] == 2
assert metadata['bit_rate'] == 128000
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata['mime'] == 'audio/mpeg'
assert metadata['mime'] == 'audio/mp3'
assert metadata['track_total'] == u'10' # MP3s can have a track_total
@ -64,7 +64,7 @@ def test_ogg_mono():
assert metadata['channels'] == 1
assert metadata['bit_rate'] == 80000
assert abs(metadata['length_seconds'] - 3.8) < 0.1
assert metadata['mime'] == 'application/ogg'
assert metadata['mime'] == 'audio/vorbis'
assert metadata['comment'] == u'Test Comment'
def test_ogg_stereo():
@ -73,7 +73,7 @@ def test_ogg_stereo():
assert metadata['channels'] == 2
assert metadata['bit_rate'] == 112000
assert abs(metadata['length_seconds'] - 3.8) < 0.1
assert metadata['mime'] == 'application/ogg'
assert metadata['mime'] == 'audio/vorbis'
assert metadata['comment'] == u'Test Comment'
''' faac and avconv can't seem to create a proper mono AAC file... ugh
@ -85,7 +85,7 @@ def test_aac_mono():
assert metadata['channels'] == 1
assert metadata['bit_rate'] == 80000
assert abs(metadata['length_seconds'] - 3.8) < 0.1
assert metadata['mime'] == 'video/mp4'
assert metadata['mime'] == 'audio/mp4'
assert metadata['comment'] == u'Test Comment'
'''
@ -95,7 +95,7 @@ def test_aac_stereo():
assert metadata['channels'] == 2
assert metadata['bit_rate'] == 102619
assert abs(metadata['length_seconds'] - 3.8) < 0.1
assert metadata['mime'] == 'video/mp4'
assert metadata['mime'] == 'audio/mp4'
assert metadata['comment'] == u'Test Comment'
def test_mp3_utf8():
@ -111,7 +111,7 @@ def test_mp3_utf8():
assert metadata['channels'] == 2
assert metadata['bit_rate'] == 128000
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata['mime'] == 'audio/mpeg'
assert metadata['mime'] == 'audio/mp3'
assert metadata['track_total'] == u'10' # MP3s can have a track_total
# Make sure the parameter checking works
@ -145,7 +145,7 @@ def test_mp3_bad_channels():
assert metadata['bit_rate'] == 64000
print metadata['length_seconds']
assert abs(metadata['length_seconds'] - 3.9) < 0.1
assert metadata['mime'] == 'audio/mpeg' # Not unicode because MIMEs aren't.
assert metadata['mime'] == 'audio/mp3' # Not unicode because MIMEs aren't.
assert metadata['track_total'] == u'10' # MP3s can have a track_total
#Mutagen doesn't extract comments from mp3s it seems