* @version $Revision$
* created Thursday May 08, 2008
* @package liberty
* @subpackage liberty_mime_handler
**/
/**
* setup
*/
global $gLibertySystem;
/**
* This is the name of the plugin - max char length is 16
* As a naming convention, the liberty mime handler definition should start with:
* PLUGIN_MIME_GUID_
*/
define( 'PLUGIN_MIME_GUID_AUDIO', 'mimeaudio' );
$pluginParams = [
// Set of functions and what they are called in this paricular plugin
// Use the GUID as your namespace
'preload_function' => 'mime_audio_preload',
'verify_function' => 'mime_default_verify',
'store_function' => 'mime_audio_store',
'update_function' => 'mime_audio_update',
'load_function' => 'mime_audio_load',
'download_function' => 'mime_default_download',
'expunge_function' => 'mime_default_expunge',
// Brief description of what the plugin does
'title' => 'Listen to uploaded Audio files',
'description' => 'This plugin will extract as much information about an uploaded audio file as possible and allow you to listen to it on the website using a streaming player.',
// Templates to display the files
'view_tpl' => 'bitpackage:liberty/mime/audio/view.tpl',
'inline_tpl' => 'bitpackage:liberty/mime/audio/inline.tpl',
'storage_tpl' => 'bitpackage:liberty/mime/audio/storage.tpl',
'attachment_tpl' => 'bitpackage:liberty/mime/audio/attachment.tpl',
'edit_tpl' => 'bitpackage:liberty/mime/audio/edit.tpl',
// url to page with options for this plugin
'plugin_settings_url' => LIBERTY_PKG_URL.'admin/plugins/mime_audio.php',
// This should be the same for all mime plugins
'plugin_type' => MIME_PLUGIN,
// Set this to true if you want the plugin active right after installation
'auto_activate' => false,
// Help page on bitweaver.org
//'help_page' => 'LibertyMime+Audio+Plugin',
// this should pick up all audio
'mimetypes' => [
'#audio/.*#i',
],
];
$gLibertySystem->registerPlugin( PLUGIN_MIME_GUID_AUDIO, $pluginParams );
/**
* mime_audio_preload This function is loaded on every page load before anything happens and is used to load required scripts.
*
* @access public
* @return void
*/
function mime_audio_preload() {
global $gBitThemes;
}
/**
* Store the data in the database
*
* @param array $pStoreRow File data needed to store details in the database - sanitised and generated in the verify function
* @access public
* @return bool true on success, false on failure - $pStoreRow['errors'] will contain reason
*/
function mime_audio_store( &$pStoreRow ) {
// this will set the correct pluign guid, even if we let default handle the store process
$pStoreRow['attachment_plugin_guid'] = PLUGIN_MIME_GUID_AUDIO;
$pStoreRow['log'] = [];
// if storing works, we process the audio
if( $ret = mime_default_store( $pStoreRow )) {
if( !mime_audio_converter( $pStoreRow )) {
// if it all goes tits up, we'll know why
$pStoreRow['errors'] = $pStoreRow['log'];
$ret = false;
}
}
return $ret;
}
/**
* mime_audio_update
*
* @param array $pStoreRow
* @param array $pParams
* @access public
* @return bool true on success, false on failure - mErrors will contain reason for failure
*/
function mime_audio_update( &$pStoreRow, $pParams = null ) {
$ret = false;
if( BitBase::verifyId( $pStoreRow['attachment_id'] )) {
$pStoreRow['log'] = [];
// set the correct pluign guid, even if we let default handle the store process
$pStoreRow['attachment_plugin_guid'] = PLUGIN_MIME_GUID_AUDIO;
// remove the entire directory
$pStoreRow['unlink_dir'] = true;
// if storing works, we process the audio
if( !empty( $pStoreRow['upload'] ) && $ret = mime_default_update( $pStoreRow )) {
if( !mime_audio_converter( $pStoreRow )) {
// if it all goes tits up, we'll know why
$pStoreRow['errors'] = $pStoreRow['log'];
$ret = false;
}
}
// if there was no upload we'll process the file parameters
if( empty( $pStoreRow['upload'] ) && !empty( $pParams['meta'] )) {
// update our local version of the file
$file = STORAGE_PKG_PATH.$pStoreRow['dest_branch'];
if( is_file( dirname( $file ).'/bitverted.mp3' )) {
$verted = dirname( $file ).'/bitverted.mp3';
} elseif( is_file( dirname( $file ).'/bitverted.m4a' )) {
$verted = dirname( $file ).'/bitverted.m4a';
}
// update audio tags of converted and original file (ignore errors since these might be m4a)
mime_audio_update_tags( $verted, $pParams['meta'] );
mime_audio_update_tags( $file, $pParams['meta'] );
// finally we update the meta table data
if( !LibertyMime::storeMetaData( $pStoreRow['attachment_id'], $pParams['meta'], 'ID3' )) {
$log['store_meta'] = "There was a problem storing the meta data in the database";
}
if( empty( $log )) {
$ret = true;
} else {
$pStoreRow['errors'] = $log;
}
}
}
return $ret;
}
/**
* Load file data from the database
*
* @param array $pFileHash Contains all file information
* @param array $pPrefs Attachment preferences taken liberty_attachment_prefs
* @param array $pParams Parameters for loading the plugin - e.g.: might contain values from the view page
* @access public
* @return array
*/
function mime_audio_load( &$pFileHash, &$pPrefs, $pParams = null ) {
global $gLibertySystem, $gBitThemes;
// don't load a mime image if we don't have an image for this file
if( $ret = mime_default_load( $pFileHash, $pPrefs )) {
// fetch meta data from the db
$ret['meta'] = LibertyMime::getMetaData( $pFileHash['attachment_id'], "ID3" );
if( !empty( $ret['storage_path'] )) {
if( is_file( dirname( STORAGE_PKG_PATH.$ret['storage_path'] ).'/bitverted.mp3' )) {
$ret['media_url'] = KernelTools::storage_path_to_url( dirname( $ret['storage_path'] )).'/bitverted.mp3';
// we need some javascript for the player:
} elseif( is_file( dirname( STORAGE_PKG_PATH.$ret['storage_path'] ).'/bitverted.m4a' )) {
$ret['media_url'] = KernelTools::storage_path_to_url( dirname( $ret['storage_path'] )).'/bitverted.m4a';
}
}
}
return $ret;
}
/**
* mime_audio_converter
*
* @param array $pParamHash
* @access public
* @return bool true on success, false on failure - mErrors will contain reason for failure
*/
function mime_audio_converter( &$pParamHash ) {
global $gBitSystem;
// audio conversion can take a while
ini_set( "max_execution_time", "1800" );
$ret = false;
$log = [];
$source = STORAGE_PKG_PATH.$pParamHash['upload']['dest_branch'].$pParamHash['upload']['name'];
$destPath = dirname( $source );
if( BitBase::verifyId( $pParamHash['attachment_id'] ?? 0 )) {
$pattern = "#.*\.(mp3|m4a)$#i";
if( !$gBitSystem->isFeatureActive( 'mime_audio_force_encode' ) && preg_match( $pattern, $pParamHash['upload']['name'] )) {
// make a copy of the original maintaining the original extension
$dest_file = $destPath.'/bitverted.'.preg_replace( $pattern, "$1", strtolower( $pParamHash['upload']['name'] ));
if( !is_file( $dest_file ) && !link( $source, $dest_file )) {
copy( $source, $dest_file );
}
$ret = true;
} else {
// TODO: have a better mechanism of converting audio to mp3. ffmpeg works well as long as the source is 'perfect'
// there are many audiofiles that can't be read by ffmpeg but by other tools like flac, faac, oggenc
// mplayer is very good, but has a lot of dependencies and not many servers have it installed
// also, using mplayer is a 2 step process: decoding and encoding
// if we convert audio, we always make an mp3
$dest_file = $destPath.'/bitverted.mp3';
if( !( $ret = mime_audio_converter_ffmpeg( $pParamHash, $source, $dest_file ))) {
// fall back to using slower mplayer / lame combo
$ret = mime_audio_converter_mplayer_lame( $pParamHash, $source, $dest_file );
}
}
// if the conversion was successful, we'll copy the tags to the new mp3 file and import data to meta tables
if( $ret == true ) {
$log['success'] = 'Successfully converted to mp3 audio';
// now that we have a new mp3 file, we might as well copy the tags accross in case someone downloads it
require_once EXTERNAL_LIBS_PATH.'getid3/getid3/getid3.php';
require_once EXTERNAL_LIBS_PATH.'getid3/getid3/getid3.lib.php';
$getID3 = new \getID3;
// we silence this since this will spew lots of ugly errors when using UTF-8 and some odd character in the file ID
$meta = @$getID3->analyze( $source );
\getid3_lib::CopyTagsToComments( $meta );
// write tags to new mp3 file
if( $errors = mime_audio_update_tags( $dest_file, $meta['comments'] )) {
$log['tagging'] = $errors;
}
// getID3 returns everything in subarrays - we want to store everything in [0]
foreach( $meta['comments'] as $key => $comment ) {
$store[$key] = $comment[0];
}
$store['playtimeseconds'] = $meta['playtime_seconds'];
$store['playtimestring'] = $meta['playtime_string'];
// make sure we remove previous entries first
LibertyMime::expungeMetaData( $pParamHash['attachment_id'] );
if( !LibertyMime::storeMetaData( $pParamHash['attachment_id'], $store, 'ID3' )) {
$log['store_meta'] = "There was a problem storing the meta data in the database";
}
// if we have an image in the id3v2 tag, we might as well do something with it
// we'll simply use the first image we can find in the file
if( !empty( $meta['id3v2']['APIC'][0]['data'] )) {
$image = $meta['id3v2']['APIC'][0];
} elseif( !empty( $meta['id3v2']['PIC'][0]['data'] )) {
$image = $meta['id3v2']['PIC'][0];
}
if ( !empty( $image )) {
// write the image to temp file for us to process
$tmpfile = str_replace( "//", "/", tempnam( TEMP_PKG_PATH, LIBERTY_PKG_NAME ));
if( $fp = fopen( $tmpfile, 'w' )) {
fwrite( $fp, $image['data'] );
fclose( $fp );
$fileHash['type'] = $image['mime'];
$fileHash['source_file'] = $tmpfile;
$fileHash['dest_branch'] = $pParamHash['upload']['dest_branch'];
liberty_generate_thumbnails( $fileHash );
// remove temp file
if( !empty( $tmpfile ) && is_file( $tmpfile )) {
unlink( $tmpfile );
}
}
}
// TODO: when tags package is enabled add an option to add tags
// recommended tags might be artist and album
// TODO: fetch album cover from amazon.com or musicbrainz.org
// fetch lyrics from lyricwiki.org
//$item->mLogs['audio_converter'] = "Audio file was successfully converted to MP3.";
}
}
// update log
$pParamHash['log'] = array_merge( $pParamHash['log'], $log );
return $ret;
}
/**
* mime_audio_converter_mplayer_lame will decode the audio to wav using mplayer and then encode to mp3 using lame
*
* @param array $pParamHash file information
* @param string $pSource source file
* @param string $pDest destination file
* @access public
* @return bool true on success, false on failure - mErrors will contain reason for failure
*/
function mime_audio_converter_mplayer_lame( &$pParamHash, $pSource, $pDest ) {
global $gBitSystem;
$ret = false;
$log = [];
if( !empty( $pParamHash ) && !empty( $pSource ) && is_file( $pSource ) && !empty( $pDest )) {
$mplayer = trim( $gBitSystem->getConfig( 'mplayer_path', shell_exec( 'which mplayer' )));
$lame = trim( $gBitSystem->getConfig( 'lame_path', shell_exec( 'which lame' )));
// confirm that both applications are available
if( $mm = shell_exec( "$mplayer 2>&1" ) && $ll = shell_exec( "$lame 2>&1" )) {
// we will decode the audio file using mplayer and encode using lame
$mplayer_params = " -quiet -vo null -vc dummy -af volume=0,resample=44100:0:1 -ao pcm:waveheader:file='$pSource.wav' '$pSource' ";
$lame_params = $gBitSystem->getConfig( "mime_audio_lame_options", " -b ".( $gBitSystem->getConfig( 'mime_audio_bitrate', 64000 ) / 1000 ))." '$pSource.wav' '$pDest' ";
$command = "$mplayer $mplayer_params && $lame $lame_params";
$debug = shell_exec( "$command 2>&1" );
// remove the temporary wav file again
@unlink( "$pSource.wav" );
// make sure the conversion was successfull
if( is_file( $pDest ) && filesize( $pDest ) > 1 ) {
$ret = true;
} else {
// remove unsuccessfully converted file
@unlink( $pDest );
$log['message'] = 'ERROR: The audio you uploaded could not be converted by mplayer and lame. DEBUG OUTPUT: '.nl2br( $debug );
// write error message to error file
$h = fopen( dirname( $pDest )."/error", 'w' );
fwrite( $h, "$command\n\n$mm\n\n$ll\n\n$debug" );
fclose( $h );
}
}
}
// update log
$pParamHash['log'] = array_merge( $pParamHash['log'], $log );
return $ret;
}
/**
* mime_audio_converter_ffmpeg
*
* @param array $pParamHash file information
* @param string $pSource source file
* @param string $pDest destination file
* @access public
* @return bool true on success, false on failure - mErrors will contain reason for failure
*/
function mime_audio_converter_ffmpeg( &$pParamHash, $pSource, $pDest ) {
global $gBitSystem;
$ret = false;
$log = [];
if( !empty( $pParamHash ) && !empty( $pSource ) && is_file( $pSource ) && !empty( $pDest )) {
// these are set in the liberty plugin admin screen
$ffmpeg = trim( $gBitSystem->getConfig( 'ffmpeg_path', shell_exec( 'which ffmpeg' )));
if( $ff = shell_exec( "$ffmpeg 2>&1" )) {
// set up parameters to convert audio
$params =
" -i '$pSource'".
" -acodec ".$gBitSystem->getConfig( 'ffmpeg_mp3_lib', 'libmp3lame' ).
" -ab ".trim( $gBitSystem->getConfig( 'mime_audio_bitrate', 64000 ).'b' ).
" -ar ".trim( $gBitSystem->getConfig( 'mime_audio_samplerate', 22050 )).
" -y '$pDest'";
$debug = shell_exec( "$ffmpeg $params 2>&1" );
// make sure the conversion was successfull
if( is_file( $pDest ) && filesize( $pDest ) > 1 ) {
$ret = true;
} else {
// remove unsuccessfully converted file
@unlink( $pDest );
$log['message'] = 'ERROR: The audio you uploaded could not be converted by ffmpeg. DEBUG OUTPUT: '.nl2br( $debug );
// write error message to error file
$h = fopen( dirname( $pDest )."/error", 'w' );
fwrite( $h, "$ffmpeg $params\n\n$ff\n\n$debug" );
fclose( $h );
}
}
}
// update log
$pParamHash['log'] = array_merge( $pParamHash['log'], $log );
return $ret;
}
/**
* mime_audio_update_tags will update the tags of a given audio file
*
* @param string $pFile absolute path to file
* @param array $pMetaData Hash of data that should be passed to the file.
* @access public
* @return null on success, String of errors on failure
*/
function mime_audio_update_tags( $pFile, $pMetaData ) {
$ret = null;
if( !empty( $pFile ) && is_file( $pFile ) && is_array( $pMetaData )) {
// we need to initiate getID3 for the writer to work
require_once EXTERNAL_LIBS_PATH.'getid3/getid3/getid3.php';
$getID3 = new \getID3;
require_once EXTERNAL_LIBS_PATH.'getid3/getid3/write.php';
// Initialize getID3 tag-writing module
$tagwriter = new getid3_writetags();
$tagwriter->filename = $pFile;
$tagwriter->tagformats = [ 'id3v1', 'id3v2.3' ];
// set various options
$tagwriter->overwrite_tags = true;
$tagwriter->tag_encoding = "UTF-8";
// prepare meta data for storing
foreach( $pMetaData as $key => $data ) {
if( !is_array( $data )) {
$data = [ $data ];
}
$write[$key] = $data;
}
// store the tags
if( !empty( $write )) {
$tagwriter->tag_data = $write;
if( !$tagwriter->WriteTags() ) {
$ret = 'Failed to write tags!
'.implode( '
', $tagwriter->errors );
}
}
}
return $ret;
}