'. t('The audio module allows users to upload and store audio files on a Drupal site. Audio is an important medium for community communication as the recent rise of the podcast phenomenon demonstrates.', array('!elink-en-wikipedia-org' => 'http://en.wikipedia.org/wiki/Podcasting')) .'

'; $help .= '

'. t('Users create audio nodes by uploading a file from their computer. They are then able to make changes to the metadata, perhaps adding an artist, or removing the track number. Visitors can download the audio file, view the file\'s metadata and encoding information, or browse for audio by metadata (artist, title, year, etc). Visitors can even play MP3s within their browser using the XSPF flash player that is bundled with the module.', array('!elink-musicplayer-sourceforge-net' => 'http://musicplayer.sourceforge.net/')) .'

'; $help .= '

'. t('The module uses the getID3 library to read and write ID3 tag information from the audio file. getID3 can read metadata from a many different audio and video formats giving the audio module a great deal of flexibility.', array('!elink-www-getid3-org' => 'http://www.getid3.org', '!elink-en-wikipedia-org' => 'http://en.wikipedia.org/wiki/Id3')) .'

'; $help .= t('

You can:

', array('!audio' => url('audio'), '!audio-by' => url('audio/by'), '!user' => url('user'), '!node-add-audio' => url('node/add/audio'), '!admin-build-block' => url('admin/build/block'), '!admin-settings-audio' => url('admin/settings/audio/audio'))); $help .= '

'. t('For more information please read the configuration and customization handbook Audio page.', array('!audio' => 'http://www.drupal.org/handbook/modules/audio/')) .'

'; return $help; case 'admin/settings/audio': $help = '

'. t('The current PHP configuration limits file uploads to %maxsize.', array('%maxsize' => format_size(file_upload_max_size()))) .'
'; $help .= '

'. t("There are two PHP ini settings, upload_max_filesize and post_max_size, that limit the maximum size of uploads. You can change these settings in the php.ini file or by using a php_value directive in Apache .htaccess file. Consult the PHP documentation for more info.") .'

'; return $help; case 'admin/settings/audio/metadata': $help = t("These settings let you determine what metadata the audio module tracks. You can add or remove metadata tags and select how they will be used. "); $help .= '

'. t('Note: deleting a tag will not remove it from the database or file until the node is saved again.') .'

'; return $help; } } /** * Implementation of hook_menu(). */ function audio_menu() { $items = array(); $items['admin/settings/audio'] = array( 'title' => 'Audio settings', 'description' => 'Change settings for the audio module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings'), 'access arguments' => array('administer site configuration'), 'file' => 'audio.admin.inc', ); $items['admin/settings/audio/main'] = array( 'title' => 'Audio', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => '-10', 'file' => 'audio.admin.inc', ); $items['admin/settings/audio/metadata'] = array( 'title' => 'Metadata tags', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings_metadata'), 'type' => MENU_LOCAL_TASK, 'file' => 'audio.admin.inc', ); $items['admin/settings/audio/players'] = array( 'title' => 'Players', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings_players'), 'type' => MENU_LOCAL_TASK, 'file' => 'audio.admin.inc', ); $items['audio/autocomplete'] = array( 'page callback' => 'audio_autocomplete', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['audio/by'] = array( 'title' => 'Browse by...', 'page callback' => 'audio_page_browse_by', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['audio/download'] = array( 'page callback' => 'audio_download', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['audio/play'] = array( 'page callback' => 'audio_play', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_theme */ function audio_theme() { return array( 'audio_file_form' => array( 'arguments' => array('form'), ), 'audio_admin_settings_metadata' => array( 'arguments' => array('form_element'), 'file' => 'audio.admin.inc', ), 'audio_admin_settings_players' => array( 'arguments' => array('form_element'), 'file' => 'audio.admin.inc', ), 'audio_default_node_player' => array( 'arguments' => array('node'), ), 'audio_teaser' => array( 'arguments' => array('node'), ), 'audio_display' => array( 'arguments' => array('node'), ), 'audio_format_tag' => array( 'arguments' => array('tag', 'value', 'setting'), ), 'audio_format_filelength' => array( 'arguments' => array('fileinfo'), ), 'audio_format_fileformat' => array( 'arguments' => array('fileinfo'), ), ); } /** * Implementation of hook_node_info(). */ function audio_node_info() { return array( 'audio' => array( 'name' => t('Audio'), 'module' => 'audio', 'description' => t('An audio file. The audio file could be used for adding music, podcasts, or audio clips to your site.'), ) ); } /** * Invoke hook_audio api methods. * * The audio module provide a hook for contributed modules. Use this rather than * the nodeapi so you don't have to worry about the module invocation order. * * function hook_audio($op, $node, $arg1) * * @param $op * The $op value will be one of the following: * 'load' * An audio node is being loaded. * 'prepare' * A node form/preview is being built. * 'submit' * A node form is being submitted, before insertion or updates. * 'insert' * A new audio node is being created. Changes you make to the node's * audio_images, audio_file, and audio_tags memebers will be saved. * 'insert revision' * A new revision of an audio node is being created. Changes you make to * the node's audio_images, and audio_file memebers will be saved. * 'update' * Changes to an audio node are being saved. Changes you make to the * node's audio_images, audio_file, and audio_tags memebers will be * saved. * 'delete' * An audio node is being deleted. Remove all of the nodes files and * database records. * 'delete revision' * A revision of the audio node is being deleted. Remove any version * specific files or database records. * 'upload' * A new audio file has been uploaded. Contrib modules can read data from * the file and append it to the audio node at this point. * 'access' * An operation is being performed on the node and other modules can * determine if this will be allowed. $arg1 will describe the operation * (i.e. 'play' or 'download'). Return a boolean if you want to allow or * deny this, or NULL if you don't. * 'download' * A user is playing the audio node. This can be handy if you're recording * user statistics. * 'play' * A user is downloading an audio node. This can be handy if you're * recording user statistics. * @param $node * An audio node object. */ function audio_invoke_audioapi($op, &$node, $a3 = NULL, $a4 = NULL) { $return = array(); foreach (module_implements('audio') as $name) { $function = $name .'_audio'; $result = $function($op, $node, $a3, $a4); if (isset($result) && is_array($result)) { $return = array_merge($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } /** * Implementation of hook_perm(). */ function audio_perm() { return array('administer audio', 'create audio', 'edit own audio', 'play audio', 'download audio', 'view download stats'); } /** * Implementation of hook_access(). */ function audio_access($op, $node = NULL) { global $user; if (user_access('administer audio')) { return TRUE; } if ($op == 'update' || $op == 'delete') { if (($user->uid == $node->uid) && user_access('edit own audio')) { return TRUE; } } if ($op == 'create') { if (user_access('create audio')) { return TRUE; } } } /** * Is the current user allowed to download an audio node? * * @return * boolean indicating if it's allowed. */ function _audio_allow_download($node) { if (isset($node->url_download) && $node->audio_file['downloadable']) { $result = audio_invoke_audioapi('access', $node, 'download'); if (in_array(TRUE, $result)) { return TRUE; } if (in_array(FALSE, $result)) { return FALSE; } return user_access('download audio'); } return FALSE; } /** * Is the current user allowed to play an audio node? * * @return * boolean indicating if it's allowed. */ function _audio_allow_play($node) { if (isset($node->url_play)) { $result = audio_invoke_audioapi('access', $node, 'play'); if (in_array(TRUE, $result)) { return TRUE; } if (in_array(FALSE, $result)) { return FALSE; } return user_access('play audio'); } return FALSE; } /** * Implements hook_cron(). * * This deletes old temp files. */ function audio_cron() { $path = audio_get_directory() .'/temp'; $files = file_scan_directory(file_create_path($path), '.*'); foreach ($files as $file => $info) { if (time() - filemtime($file) > 60*60*6) { file_delete($file); } } } /** * Implementation of hook_link(). */ function audio_link($type, $node, $main = 0) { $links = array(); $link_access = user_access('view download stats'); if ($type == 'node' && $node->type == 'audio') { if (_audio_allow_download($node)) { $links['audio_download_link'] = array( 'title' => t('Download audio file'), 'href' => $node->url_download, ); if ($link_access) { $links['audio_download_count'] = array( 'title' => t('@download_count downloads', array('@download_count' => $node->audio_file['download_count'])), ); } } if (_audio_allow_play($node) && $link_access) { $links['audio_play_count'] = array( 'title' => t('@play_count plays', array('@play_count' => $node->audio_file['play_count'])), ); } } return $links; } /** * Implementation of hook_nodeapi(). */ function audio_nodeapi(&$node, $op, $arg) { if ($node->type == 'audio') { switch ($op) { case 'delete revision': audio_delete_revision($node); break; case 'rss item': $ret = array(); if (_audio_allow_download($node)) { // Reset the node's body to remove theming. $body = db_result(db_query("SELECT r.body FROM {node} n INNER JOIN {node_revisions} r ON n.nid = r.nid WHERE n.vid=%d", $node->vid)); $node->body = $body; $node = node_prepare($node, FALSE); $ret[] = array( 'key' => 'enclosure', 'attributes' => array( 'url' => $node->url_download, 'length' => $node->audio_file['file_size'], 'type' => $node->audio_file['file_mime'] )); // Provide very basic iTunes support. $ret[] = array( 'namespace' => array('itunes' => 'xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"'), ); $ret[] = array( 'key' => 'itunes:duration', 'value' => $node->audio_file['playtime'], ); $ret[] = array( 'key' => 'itunes:author', 'value' => $node->audio_tags['artist'], ); } return $ret; case 'update index': // Since the theme might hide the tag values we'll manually add them to // the search index. $cleantags = array(); foreach ($node->audio_tags as $value) { $cleantags[] = check_plain($value); } return $cleantags; } } } /** * Implementation of hook_view(). */ function audio_view(&$node, $teaser = FALSE, $page = FALSE) { drupal_add_css(drupal_get_path('module', 'audio') .'/audio.css'); $node = node_prepare($node, $teaser); if ($teaser) { $node->content['audio_info'] = array( '#value' => theme('audio_teaser', $node), '#weight' => 0, ); } else { $node->content['audio_info'] = array( '#value' => theme('audio_display', $node), '#weight' => -1, ); } return $node; } /** * Implementation of hook_validate(). */ function audio_validate(&$node, &$form) { $nid = ($node->nid) ? $node->nid : 'new_node'; if (!isset($node->audio_file['file_path']) && !isset($_SESSION['audio_new_file'][$nid])) { form_set_error('audio_upload', t("A file must be provided. If you tried uploading a file, make sure it's less than the upload size limit.")); } // Build the title from metadata. If there's not title format use the default. if (empty($node->title_format)) { $node->title_format = variable_get('audio_default_title_format', '[audio-tag-title-raw] by [audio-tag-artist-raw]'); } $node->title = token_replace($node->title_format, 'node', $node); $form_state['values']['title'] = $node->title; // FIXME } /** * Implementation of hook_load(). */ function audio_load($node) { if ($node->vid) { // This is a wonky way to load the fields but its handy right now while // I'm renaming fields in the databases. $fields = db_fetch_array(db_query("SELECT * FROM {audio} WHERE vid=%d", $node->vid)); $ret = array( 'title_format' => $fields['title_format'], 'audio_file' => array( 'play_count' => $fields['play_count'], 'download_count' => $fields['download_count'], 'downloadable' => $fields['downloadable'], 'file_format' => $fields['file_format'], 'file_mime' => $fields['file_mime'], 'file_name' => $fields['file_name'], 'file_path' => $fields['file_path'], 'file_size' => $fields['file_size'], 'sample_rate' => $fields['sample_rate'], 'channel_mode' => $fields['channel_mode'], 'bitrate' => $fields['bitrate'], 'bitrate_mode' => $fields['bitrate_mode'], 'playtime' => $fields['playtime'], 'bitrate' => $fields['bitrate'], ), ); if (file_exists($fields['file_path'])) { // TODO: should these links be by vid? $ret['url_play'] = url('audio/play/'. $node->nid, array('absolution' => TRUE)); if ($ret['audio_file']['downloadable']) { // iTunes and other podcasting programs check the url to determine the // file type. we'll add the original file name on to the end. see issues // #35398 and #68716 for more info. $url = 'audio/download/'. $node->nid .'/'. $fields['file_name']; $ret['url_download'] = url($url, array('absolute' => TRUE)); } } // Load the audio tags. $result = db_query("SELECT tag, value FROM {audio_metadata} WHERE vid=%d", $node->vid); $ret['audio_tags'] = array(); while ($obj = db_fetch_object($result)) { $ret['audio_tags'][$obj->tag] = $obj->value; } // Allow other modules to access newly loaded audio node. return array_merge($ret, audio_invoke_audioapi('load', $node)); } return array(); } /** * Implementation of hook_insert(). */ function audio_insert(&$node) { drupal_set_message('Hammy hoo...?'); file_move($node->audio_file['file_path'], audio_get_directory(), FILE_EXISTS_RENAME); // Notify other modules. audio_invoke_audioapi('insert', $node); $f = $node->audio_file; db_query("INSERT INTO {audio} (vid, nid, title_format, downloadable, file_format, file_name, file_path, file_mime, file_size, bitrate, bitrate_mode, sample_rate, channel_mode, playtime) VALUES (%d, %d, '%s', %d, '%s', '%s', '%s', '%s', %d, %f, '%s', %d, '%s', '%s')", $node->vid, $node->nid, $node->title_format, $f['downloadable'], $f['file_format'], $f['file_name'], $f['file_path'], $f['file_mime'], filesize($f['file_path']), $f['bitrate'], $f['bitrate_mode'], $f['sample_rate'], $f['channel_mode'], $f['playtime']); _audio_save_tags_to_db($node); // Unset the new file flag incase this node is re-saved. unset($node->audio_file['newfile']); } /** * Insert a new node revision. * * If a new file wasn't uploaded, make a copy of the existing file. */ function audio_insert_revision($node) { if (isset($node->audio_file['newfile'])) { file_move($node->audio_file['file_path'], audio_get_directory(), FILE_EXISTS_RENAME); } else { $newname = file_create_filename($node->audio_file['file_name'], audio_get_directory()); file_copy($node->audio_file['file_path'], $newname); } // Notify other modules. audio_invoke_audioapi('insert revision', $node); $f = $node->audio_file; db_query("INSERT INTO {audio} (vid, nid, title_format, downloadable, file_format, file_name, file_path, file_mime, file_size, bitrate, bitrate_mode, sample_rate, channel_mode, playtime) VALUES (%d, %d, '%s', %d, '%s', '%s', '%s', '%s', %d, %f, '%s', %d, '%s', '%s')", $node->vid, $node->nid, $node->title_format, $f['downloadable'], $f['file_format'], $f['file_name'], $f['file_path'], $f['file_mime'], filesize($f['file_path']), $f['bitrate'], $f['bitrate_mode'], $f['sample_rate'], $f['channel_mode'], $f['playtime']); _audio_save_tags_to_db($node); } /** * Implementation of hook_update(). */ function audio_update(&$node) { // Check for new revsion. if ($node->revision) { return audio_insert_revision($node); } if (isset($node->audio_file['newfile'])) { unset($node->audio_file['newfile']); // Remove the old file. $oldfile = db_fetch_object(db_query('SELECT file_path FROM {audio} f WHERE f.vid = %d', $node->vid)); file_delete($oldfile->file_path); // Save the new one. file_move($node->audio_file['file_path'], audio_get_directory(), FILE_EXISTS_RENAME); } // Notify other modules. audio_invoke_audioapi('update', $node); // Do a delete and insert rather than an update to ensure that we've got db // records. it takes a bit longer but it allows us to carry on if one of the // records wasn't created by audio_insert(). db_query("DELETE FROM {audio} WHERE vid=%d", $node->vid); $f = $node->audio_file; db_query("INSERT INTO {audio} (vid, nid, title_format, play_count, download_count, downloadable, file_format, file_name, file_path, file_mime, file_size, bitrate, bitrate_mode, sample_rate, channel_mode, playtime) VALUES (%d, %d, '%s', %d, %d, %d, '%s', '%s', '%s', '%s', %d, %f, '%s', %d, '%s', '%s')", $node->vid, $node->nid, $node->title_format, $f['play_count'], $f['download_count'], $f['downloadable'], $f['file_format'], $f['file_name'], $f['file_path'], $f['file_mime'], filesize($f['file_path']), $f['bitrate'], $f['bitrate_mode'], $f['sample_rate'], $f['channel_mode'], $f['playtime']); _audio_save_tags_to_db($node); } /** * Implementation of hook_delete(). */ function audio_delete($node) { // Notify other modules. audio_invoke_audioapi('delete', $node); $result = db_query('SELECT vid, file_path FROM {audio} WHERE nid = %d', $node->nid); while ($o = db_fetch_object($result)) { if (isset($o->file_path)) { file_delete($o->file_path); } db_query('DELETE FROM {audio_metadata} WHERE vid = %d', $o->vid); } db_query('DELETE FROM {audio} WHERE nid = %d', $node->nid); } /** * Delete a single revision. */ function audio_delete_revision($node) { // Notify other modules. audio_invoke_audioapi('delete revision', $node); if ($filepath = db_result(db_query('SELECT file_path FROM {audio} WHERE vid = %d', $node->vid))) { file_delete($filepath); } db_query('DELETE FROM {audio_metadata} WHERE vid = %d', $node->vid); db_query('DELETE FROM {audio} WHERE vid = %d', $node->vid); } /** * Implementation of hook_prepare(). * * We do a lot here because we need to read the file's meta data after upload * but before both the form and node preview are built. */ function audio_prepare(&$node) { // We need to be aware that a user may try to edit multiple audio nodes at // once. By using the $nid variable each node's files can be stored separately // in the session. $nid = empty($node->nid) ? 'new_node' : $node->nid; // When you enter the edit view the first time we need to clear our files in // session for this node. This is so if you upload a file, then decide you // don't want it and reload the form (without posting), the files will be // discarded. if (count($_POST) == 0) { unset($_SESSION['audio_new_file'][$nid]); } // Check for an upload. $validators = array( 'file_validate_extensions' => variable_get('audio_allowed_extensions', 'mp3 wav ogg'), ); if ($file = file_save_upload('audio_upload', $validators)) { file_copy($file, 'files/audio/'. $file->filename, FILE_EXISTS_REPLACE); $node->audio_file = array( 'newfile' => TRUE, 'play_count' => 0, 'download_count' => 0, 'downloadable' => (bool) $_POST['audio_file']['downloadable'], 'file_name' => $file->filename, 'file_path' => $file->filepath, 'file_mime' => $file->filemime, 'file_size' => filesize($node->audio_file['file_path']), ); // Allow other modules to modify the node. audio_invoke_audioapi('upload', $node); // ...save info to $_POST so that it shows up in both the preview and // form. Note that we do this after calling we called our api hook with // the upload operation, it gives the audio_id3 module a chance to read // the tags. $_POST['audio_file'] = $node->audio_file; $_POST['audio_tags'] = $node->audio_tags; $_SESSION['audio_new_file'][$nid] = $node->audio_file['file_path']; } // Assign a newly uploaded file that's being previewed. else if (isset($_SESSION['audio_new_file'][$nid])) { $node->audio_file['file_path'] = $_SESSION['audio_new_file'][$nid]; } audio_invoke_audioapi('prepare', $node); } /** * Implementation of hook_form(). */ function audio_form(&$node) { $type = node_get_types('type', $node); if ($type->has_title) { $form['title']['#weight'] = -5; $form['title']['title_format'] = array( '#type' => 'textfield', '#title' => check_plain($type->title_label), '#default_value' => !empty($node->title_format) ? $node->title_format : variable_get('audio_default_title_format', '[audio-tag-title-raw] by [audio-tag-artist-raw]'), '#description' => t("The title can use the file's metadata. You can use the tokens listed below to insert information into the title. Note: the node title is escaped so it is safe to use the -raw tokens."), '#required' => TRUE, ); $form['title']['token_help'] = array( '#title' => t('Token list'), '#type' => 'fieldset', '#collapsible' => TRUE, '#collapsed' => TRUE, '#description' => t('This is a list of the tokens that can be used in the title of audio nodes.'), 'help' => array('#value' => theme('token_help', 'node')), ); } if ($type->has_body) { $form['body_filter']['#weight'] = -4; $form['body_filter']['body'] = array( '#type' => 'textarea', '#title' => check_plain($type->body_label), '#default_value' => $node->body, '#rows' => 5, '#required' => ($type->min_word_count > 0), ); $form['body_filter']['format'] = filter_form($node->format); } $form['#attributes'] = array('enctype' => 'multipart/form-data'); if (empty($node->nid)) { $node->audio_file = array( 'file_path' => '', 'file_name' => '', 'file_mime' => '', ); } $form['audio_file'] = array( '#type' => 'fieldset', '#title' => t('Audio File Info'), '#collapsible' => TRUE, '#weight' => -1, '#tree' => TRUE, ); // Store the non-user editable file information as values. $form['audio_file']['file_path'] = array( '#type' => 'value', '#value' => $node->audio_file['file_path'], ); $form['audio_file']['file_name'] = array( '#type' => 'value', '#value' => $node->audio_file['file_name'], ); $form['audio_file']['file_mime'] = array( '#type' => 'value', '#value' => $node->audio_file['file_mime'], ); // Place a visible copy of the file path on the form (after removing the // directory info from non-admins). $file_path = $node->audio_file['file_path']; if (!user_access('administer audio')) { $file_path = basename($file_path); } $form['audio_file']['display_file_path'] = array( '#type' => 'item', '#title' => t('Current File'), '#value' => !empty($file_path) ? $file_path : t('No file is attached.'), ); // If we've got a file, add the file information fields. if (!empty($node->audio_file['file_name'])) { $form['audio_file']['#theme'] = 'audio_file_form'; $form['audio_file']['file_format'] = array( '#type' => 'select', '#title' => t('Format'), '#default_value' => $node->audio_file['file_format'], '#options' => drupal_map_assoc(array('', 'aac', 'ac3', 'au', 'avr', 'flac', 'midi', 'mod', 'mp3', 'mpc', 'ogg', 'voc'), 'drupal_strtoupper'), ); $form['audio_file']['file_size'] = array( '#type' => 'textfield', '#title' => t('File Size'), '#default_value' => $node->audio_file['file_size'], ); $form['audio_file']['playtime'] = array( '#type' => 'textfield', '#title' => t('Length'), '#default_value' => $node->audio_file['playtime'], '#description' => t('The format is hours:minutes:seconds.'), ); $form['audio_file']['sample_rate'] = array( '#type' => 'select', '#title' => t('Sample rate'), '#default_value' => $node->audio_file['sample_rate'], '#options' => array('' => '', '48000' => '48,000 Hz', '44100' => '44,100 Hz', '32000' => '32,000 Hz', '22050' => '22,050 Hz', '11025' => '11,025 Hz', '8000' => '8,000 Hz'), ); $form['audio_file']['channel_mode'] = array( '#type' => 'select', '#title' => t('Channel mode'), '#default_value' => $node->audio_file['channel_mode'], '#options' => array('stereo' => t('Stereo'), 'mono' => t('Mono')), ); $form['audio_file']['bitrate'] = array( '#type' => 'textfield', '#title' => t('Bitrate'), '#default_value' => $node->audio_file['bitrate'], ); $form['audio_file']['bitrate_mode'] = array( '#type' => 'select', '#title' => t('Bitrate mode'), '#default_value' => $node->audio_file['bitrate_mode'], '#options' => array('' => '', 'cbr' => t('Constant'), 'vbr' => t('Variable')), ); // Users shouldn't be able to change the play and download counts so we'll // put these for viewing... $form['audio_file']['display_play_count'] = array( '#type' => 'item', '#title' => t('Play count'), '#value' => $node->audio_file['play_count'], ); $form['audio_file']['display_download_count'] = array( '#type' => 'item', '#title' => t('Download count'), '#value' => $node->audio_file['download_count'], ); // ...and these are what we'll save back to the node. $form['audio_file']['play_count'] = array( '#type' => 'value', '#value' => $node->audio_file['play_count'], ); $form['audio_file']['download_count'] = array( '#type' => 'value', '#value' => $node->audio_file['download_count'], ); } $form['audio_file']['audio_upload'] = array( '#tree' => FALSE, '#type' => 'file', '#title' => empty($node->audio_file['file_name']) ? t('Add a new audio file') : t('Replace this with a new file'), '#description' => t('Click "Browse..." to select an audio file to upload. Only files with the following extensions are allowed: %allowed-extensions.', array('%allowed-extensions' => variable_get('audio_allowed_extensions', 'mp3 wav ogg'))) .'
' . t('NOTE: the current PHP configuration limits uploads to %maxsize.', array('%maxsize' => format_size(file_upload_max_size()))), ); $form['audio_file']['downloadable'] = array( '#type' => 'checkbox', '#title' => t('Allow file downloads.'), '#default_value' => !empty($node->audio_file['downloadable']) ? $node->audio_file['downloadable'] : variable_get('audio_default_downloadable', 1), '#description' => t('If checked, a link will be displayed allowing visitors to download this audio file on to their own computer.') .'
' . t('WARNING: even if you leave this unchecked, clever users will be able to find a way to download the file. This just makes them work a little harder to find the link.'), ); // If we've got a file, add the fields for editing meta data. if (!empty($node->audio_file)) { $form['audio_tags'] = array( '#type' => 'fieldset', '#title' => t('Audio Metadata'), '#collapsible' => TRUE, '#tree' => TRUE, '#weight' => -3, ); // Delegate out the dirty work of building form elements. foreach (audio_get_tag_settings() as $tag => $tag_settings) { $form['audio_tags'][$tag] = _audio_build_tag_form($tag, $tag_settings, empty($node->audio_tags) ? '' : $node->audio_tags[$tag]); } } // Register the submit hander. $form['#submit'][] = 'audio_node_submit'; return $form; } /** * Theme function to put the form elements into a table. Steal some CSS from * the watchdog module. */ function theme_audio_file_form($form) { // List of elements to leave out of the table. $skip_list = array('audio_upload' => 1, 'downloadable' => 1); $rows = array(); foreach (element_children($form) as $key) { if (!isset($skip_list[$key])) { if ($form[$key]['#type'] != 'hidden' && $form[$key]['#type'] != 'value') { $title = $form[$key]['#title']; unset($form[$key]['#title']); $rows[] = array( array('data' => $title, 'header' => TRUE), drupal_render($form[$key]), ); } } } $output = theme('table', array(), $rows, array('class' => 'watchdog-event')); return $output . drupal_render($form); } /** * Construct a form element to represent a tag. * * @param $tag * Name of the tag. * @param $tag_settings * Array of settings that determine the way the tag is handled. * @param $value * Value of the tag. * @return * Array with a form element. */ function _audio_build_tag_form($tag, $tag_settings, $value) { $form_element = array( '#type' => 'textfield', '#title' => t(ucwords(str_replace('_', ' ', $tag))), '#default_value' => $value, '#maxlength' => 255, '#required' => (boolean) $tag_settings['required'], ); if ($tag_settings['autocomplete']) { $form_element['#autocomplete_path'] = 'audio/autocomplete/'. $tag; } switch ($tag) { case 'track': $form_element['#title'] = t('Track Number'); $form_element['#maxlength'] = 10; $form_element['#description'] = t("Enter either a single number or fraction here. '1' means that this is the first track on the album, and '1/8' ."); break; case 'genre': $form_element['#maxlength'] = 40; break; } return $form_element; } /** * Node submit handler */ function audio_node_submit($form, &$form_state) { // If a new file was stored in the session move it into the $form_state. $nid = ($form_state['values']['nid']) ? $form_state['values']['nid'] : 'new_node'; if (isset($_SESSION['audio_new_file'][$nid])) { $form_state['values']['audio_file']['file_path'] = $_SESSION['audio_new_file'][$nid]; $form_state['values']['audio_file']['newfile'] = TRUE; unset($_SESSION['audio_new_file'][$nid]); } audio_invoke_audioapi('submit', $node); } /** * Implementation of hook_user(). */ function audio_user($op, &$edit, &$user, $category = NULL) { if ($op == 'view') { $result = db_query(db_rewrite_sql("SELECT COUNT(*) FROM {node} n WHERE n.type = 'audio' AND n.uid = %d AND n.status = 1"), $user->uid); if (db_result($result) > 0) { $items['audio'] = array( 'title' => t('Audio'), 'value' => l(t("Listen to @username's recent audio files", array('@username' => $user->name)), 'audio/user/'. $user->uid), 'class' => 'audio', ); return array(t('History') => $items); } } } /** * Implementation of hook_block(). * * Provides a block for browsing by metadata. */ function audio_block($op = 'list', $delta = 0, $edit = array()) { switch ($op) { case 'list': $blocks[2]['info'] = t('audio: Browse by'); return $blocks; case 'view': if (user_access('access content')) { switch ($delta) { case 2: $items = array(); $settings = audio_get_tag_settings(); $result = db_query(db_rewrite_sql('SELECT DISTINCT a.tag FROM {node} n INNER JOIN {audio_metadata} a ON n.vid = a.vid WHERE n.status = 1 ORDER BY a.tag ASC')); while ($obj = db_fetch_object($result)) { if ($settings[$obj->tag]['browsable']) { $items[] = l($obj->tag, 'audio/by/'. $obj->tag); } } $block['subject'] = t('Browse for audio by'); $block['content'] = theme('item_list', $items); break; } return $block; } } } /** * Get an array of the allowed tags. * * @return * Array of allowed tags. */ function audio_get_tags_allowed() { return array_keys(audio_get_tag_settings()); } /** * Get an array of the tags and their settings. * * @return * Array of allowed tags. */ function audio_get_tag_settings() { $defaults = array( 'artist' => array('autocomplete' => 1, 'required' => 1, 'hidden' => 0, 'writetofile' => 1, 'browsable' => 1, 'weight' => -2), 'title' => array('autocomplete' => 1, 'required' => 1, 'hidden' => 0, 'writetofile' => 1, 'browsable' => 1, 'weight' => -2), 'album' => array('autocomplete' => 1, 'required' => 0, 'hidden' => 0, 'writetofile' => 1, 'browsable' => 1, 'weight' => -1), 'track' => array('autocomplete' => 0, 'required' => 0, 'hidden' => 0, 'writetofile' => 1, 'browsable' => 0, 'weight' => -1), 'genre' => array('autocomplete' => 1, 'required' => 0, 'hidden' => 0, 'writetofile' => 1, 'browsable' => 1, 'weight' => 0), 'year' => array('autocomplete' => 0, 'required' => 0, 'hidden' => 0, 'writetofile' => 1, 'browsable' => 1, 'weight' => 1), ); return variable_get('audio_tag_settings', $defaults); } /** * Retrieve autocomplete suggestions for existing audio metadata tags. * * @param $tag * tag listed in audio_get_tag_settings() where autocomplete is true. * @param $value * partial tag value to try to find in the database. */ function audio_autocomplete($tag = '', $value = '') { $matches = array(); $tags = audio_get_tag_settings(); if (isset($tags[$tag]) && $tags[$tag]['autocomplete']) { $result = db_query_range("SELECT DISTINCT a.value FROM {audio_metadata} a WHERE a.tag = '%s' AND LOWER(a.value) LIKE LOWER('%s%%') ORDER BY a.value", $tag, $value, 0, 10); while ($tag = db_fetch_object($result)) { $matches[$tag->value] = check_plain($tag->value); } } print drupal_to_js($matches); exit(); } /** * Get the audio files directory. * * @return * string path to the audio directory */ function audio_get_directory() { // We have to make sure the files directory exists first... $dir = file_directory_path(); file_check_directory($dir, FILE_CREATE_DIRECTORY, 'made up element name'); // ...then that our audio sub-directory exists. $dir .= '/audio'; file_check_directory($dir, FILE_CREATE_DIRECTORY, 'made up element name'); return $dir; } /** * Provide a way to browse the audio by metadata. * * @param $tag * ID3 tag name. * @param $value * Value of the tag. It'll be "cleaned" of non-alphanumeric values characters * comparing it. */ function audio_page_browse_by($tag = NULL, $value = NULL) { $breadcrumb = array(l(t('Audio'), 'audio')); if (isset($tag)) { $breadcrumb[] = l(t('Browse by'), 'audio/by'); if (isset($value)) { drupal_set_title(t("audio by @tag %value", array('@tag' => $tag, '%value' => trim(str_replace('_', ' ', $value))))); $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, a.tag, a.value FROM {node} n INNER JOIN {audio_metadata} a ON n.vid = a.vid WHERE a.tag = '%s' AND a.clean = '%s' AND n.status = 1 ORDER BY n.created ASC"), $tag, audio_clean_tag($value)); $breadcrumb[] = l(t('@tag', array('@tag' => $tag)), 'audio/by/'. $tag); $output = ''; while ($obj = db_fetch_object($result)) { if ($node = node_load($obj->nid)) { $output .= node_view($node, TRUE); } } } else { drupal_set_title(t("Audio by @tag", array('@tag' => $tag))); $result = db_query(db_rewrite_sql("SELECT a.value, a.clean, COUNT(a.clean) AS count FROM {node} n INNER JOIN {audio_metadata} a ON n.vid = a.vid WHERE a.tag = '%s' AND n.status = 1 GROUP BY a.value, a.clean ORDER BY a.value ASC"), $tag); while ($obj = db_fetch_object($result)) { $items[] = l($obj->value, "audio/by/{$tag}/{$obj->clean}") ." ({$obj->count})"; } $output = theme('item_list', $items); } } else { drupal_set_title(t("Browse for audio by...")); $settings = audio_get_tag_settings(); $result = db_query(db_rewrite_sql('SELECT DISTINCT a.tag FROM {node} n INNER JOIN {audio_metadata} a ON n.vid = a.vid WHERE n.status = 1 ORDER BY a.tag ASC')); while ($obj = db_fetch_object($result)) { if ($settings[$obj->tag]['browsable']) { $items[] = l($obj->tag, 'audio/by/'. $obj->tag); } } $output = theme('item_list', $items); } drupal_set_breadcrumb($breadcrumb); return $output; } /** * Loads the player plugs-ins and returns their details. * * @return * Array with two sub arrays. The first keyed by player name, the second * keyed by format and then player name. The same player may be listed for * multiple formats. */ function _audio_player_build_list() { // Invoke any modules that implement the hook. $players_name = module_invoke_all('audio_player'); // Load all our module's player plugins. $path = drupal_get_path('module', 'audio') .'/players'; $files = drupal_system_listing('.inc$', $path, 'name', 0); foreach ($files as $file) { require_once('./'. $file->filename); $function = 'audio_'. $file->name .'_audio_player'; if (function_exists($function)) { $result = $function(); if (isset($result) && is_array($result)) { $players_name = array_merge($players_name, $result); } } } // Group players by format. $players_format = array(); foreach ($players_name as $name => $player) { foreach ($player['formats'] as $format) { $players_format[$format][$name] = $player; } } return array($players_name, $players_format); } /** * Return information on the installed player plugins. * * @param $op * String specifying the operation. Possible values are: * 'formats' * 'format' * 'names' * 'name' * @param $name * Name of a specific player, used with $op = 'format' or 'name'. */ function audio_get_players($op = 'names', $name = '') { static $_player_name, $_player_format; if (!isset($_player_format)) { list($_player_name, $_player_format) = _audio_player_build_list(); } switch ($op) { case 'formats': return $_player_format; case 'format': if (isset($_player_format[$name])) { return $_player_format[$name]; } return FALSE; case 'names': return $_player_name; case 'name': if (isset($_player_name[$name])) { return $_player_name[$name]; } return FALSE; default: return FALSE; } } /** * Build HTML to play an audio node. * * @param $node * Node object. * @param $playername * Optional, string specifying the name of the desired player. * @return * HTML to play the audio tracks. */ function audio_get_node_player($node, $playername = NULL) { $output = ''; if (_audio_allow_play($node)) { $format = $node->audio_file['file_format']; if (!isset($playername)) { $playername = variable_get('audio_player_'. $format, '1pixelout'); } $player = audio_get_players('name', $playername); // Try to use the requested player... $output = theme($player['theme_node'], $node); if (!$output) { // ...if that doesn't work out, use the generic one. $output = theme('audio_default_node_player', $node); } } return $output; } /** * Provide a link to the audio file. * * @param $node * Node object. */ function theme_audio_default_node_player($node) { return ''. l(t('Click to play'), $node->url_play) ."\n"; } /** * Take a tag and force it to be a-z 0-9 _ - * * @param $string * ID3 tag value * @return * cleaned up tag value for URL or database */ function audio_clean_tag($string) { // If we've got characters besides 0-9 A-Z a-z hyphen and underscore, replace // them. if (preg_match('/[^-\w]/', $string)) { // Remove accents... $string = strtr($string, 'ŠŽšžŸÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ', 'SZszYAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy'); // ...convert to equivalent chars... $string = strtr($string, array('Þ' => 'TH', 'þ' => 'th', 'Ð' => 'DH', 'ð' => 'dh', 'ß' => 'ss', 'Œ' => 'OE', 'œ' => 'oe', 'Æ' => 'AE', 'æ' => 'ae', 'µ' => 'u')); // ... and remove anything else that's not alphanumeric and replace it with an underscore. $string = preg_replace('/[^-\w]+/', '_', $string); } // Remove leading and trailing underscores. $string = trim($string, '_'); // Finally, make it to lower case. return strtolower($string); } /** * Fetches an audio file, allows "shorthand" urls such of the form: * audio/view/$nid (e.g. audio/download/25/ or audio/download/14) * * @param $nid * Node ID. */ function audio_download($nid = FALSE) { if ($nid && $node = node_load($nid)) { if (_audio_allow_download($node)) { // Increment the play count. db_query('UPDATE {audio} SET download_count = download_count + 1 WHERE vid = %d', $node->vid); // Downloading counts as "viewing" the node. node_tag_new($node->nid); // Notify other modules. audio_invoke_audioapi('download', $node); // The mime_header_encode function does not (yet) support // quoted-string encoding of ASCII strings with special // characters. See discussion at http://drupal.org/node/82614 $filename = $node->audio_file['file_name']; // If the string contains non-ASCII characters, process it through // the mime_header_encode function. if (preg_match('/[^\x20-\x7E]/', $filename)) { $filename = mime_header_encode($filename); } // Otherwise, if the string contains special characters (like // space), perform quoted-string encoding. elseif (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $filename)) { $filename = '"'. str_replace('"', '\"', $filename) .'"'; } $headers = array( 'Content-Type: '. mime_header_encode($node->audio_file['file_mime']), 'Content-Length: '. $node->audio_file['file_size'], 'Content-Disposition: attachment; filename='. $filename, ); audio_file_transfer($node->audio_file['file_path'], $headers); } drupal_access_denied(); } else { drupal_not_found(); } } /** * Similar to audio_download, but for streaming playback instead of download. * * @param $nid * Node ID. */ function audio_play($nid = FALSE) { if ($nid && $node = node_load($nid)) { if (_audio_allow_play($node)) { // Increment the play count. db_query('UPDATE {audio} SET play_count = play_count + 1 WHERE vid = %d', $node->vid); // Playing counts as "viewing" the node. node_tag_new($node->nid); // Notify other modules. audio_invoke_audioapi('play', $node); $headers = array( 'Pragma: public', 'Expires: 0', 'Cache-Control: must-revalidate, post-check=0, pre-check=0, private', 'Content-Type: '. mime_header_encode($node->audio_file['file_mime']), 'Content-Length: '. $node->audio_file['file_size'], 'Content-Disposition: inline;', 'Content-Transfer-Encoding: binary', ); // Required for IE, otherwise Content-disposition is ignored. ini_set('zlib.output_compression', 'Off'); audio_file_transfer($node->audio_file['file_path'], $headers); } drupal_access_denied(); } else { drupal_not_found(); } } /** * Transfer a file to the client. * * Variation on Drupal's file_transfer() function. The only difference * is that set_time_limit() is called to allow for large files. * * @param $source * String specifying the file to transfer. * @param $headers * An array of http headers to send along with file. */ function audio_file_transfer($source, $headers) { ob_end_clean(); foreach ($headers as $header) { // To prevent HTTP header injection, we delete new lines that are // not followed by a space or a tab. // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 $header = preg_replace('/\r?\n(?!\t| )/', '', $header); header($header); } $source = file_create_path($source); // Transfer file in 1024 byte chunks to save memory usage. if ($fd = fopen($source, 'rb')) { if (!ini_get('safe_mode')) { set_time_limit(0); } while (!feof($fd)) { print fread($fd, 1024); ob_flush(); flush(); } fclose($fd); } else { drupal_not_found(); } exit(); } /** * Create audio nodes from a file. * * Function for other modules to use to create a audio node from a file. Once * you've created it you can make any changes and then save it using * node_save(). * * @param $filepath * Full path to an audio file. be aware that the file will be moved into * drupal's directory. * @param $title_format * A t() formatting string for generating the node's title. you can use any * value in the node's audio_tags array as variable. if nothing is provided * the default title format will be used. * @param $tags * An, optional, array of metadata to add to the node. these will be * available to the $title_format. * @return * A node or FALSE on error. */ function audio_api_insert($filepath, $title_format = NULL, $tags = array()) { global $user; // Check for user permission. if (!audio_access('create')) { drupal_access_denied(); } $filepath = realpath($filepath); $node = new stdClass(); $node->type = 'audio'; $node->uid = $user->uid; $node->name = $user->name; // Set the node's defaults... (copied this from node and comment.module) $node_options = variable_get('node_options_'. $node->type, array('status', 'promote')); $node->status = in_array('status', $node_options); $node->moderate = in_array('moderate', $node_options); $node->promote = in_array('promote', $node_options); if (module_exists('comment')) { $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE); } $node->audio_tags = array(); $node->audio_images = array(); $node->audio_file = array( 'newfile' => TRUE, 'downloadable' => variable_get('audio_default_downloadable', 1), 'play_count' => 0, 'download_count' => 0, 'file_size' => filesize($filepath), 'file_name' => basename($filepath), 'file_path' => $filepath, 'file_mime' => 'audio/mpeg', // This may be a bad guess... ); if (function_exists('mime_content_type')) { $node->audio_file['file_mime'] = mime_content_type($filepath); } // Allow other modules to modify the node (hopefully reading in tags). audio_invoke_audioapi('upload', $node); // Add the tags (overwriting any that audio_getid3 may have loaded). if ($tags) { $node->audio_tags = $tags; } // Build the title manually (since we don't call node_validate()). $node->title_format = $title_format; if (empty($node->title_format)) { $node->title_format = variable_get('audio_default_title_format', '[audio-tag-title-raw] by [audio-tag-artist-raw]'); } // Flush the token cache, otherwise when importing multiple nodes they'll all // have the same title. token_get_values('node', $node, TRUE); $node->title = token_replace($node->title_format, 'node', $node); // Save it. $node = node_submit($node); node_save($node); return $node; } /** * Save the node's ID3 tags into the database. * * @param $node * Node object. */ function _audio_save_tags_to_db($node) { $allowed_tags = audio_get_tags_allowed(); // Remove any existing metadata. db_query("DELETE FROM {audio_metadata} WHERE vid=%d", $node->vid); // Save the new tags. foreach ($node->audio_tags as $tag => $value) { if (in_array($tag, $allowed_tags) && $value) { db_query("INSERT INTO {audio_metadata} (vid, tag, value, clean) VALUES (%d, '%s', '%s', '%s')", $node->vid, $tag, $value, audio_clean_tag($value)); } } } /** * Parse an array into a valid urlencoded query string. * * This function is a work-around for a drupal_urlencode issue in core. * See: http://drupal.org/node/158687 for details. * * @param $query * The array to be processed e.g. $_GET. * @param $exclude * The array filled with keys to be excluded. Use parent[child] to exclude * nested items. * @param $parent * Should not be passed, only used in recursive calls. * @return * urlencoded string which can be appended to/as the URL query string. */ function audio_query_string_encode($query, $exclude = array(), $parent = '') { $params = array(); foreach ($query as $key => $value) { $key = urlencode($key); if ($parent) { $key = $parent .'['. $key .']'; } if (in_array($key, $exclude)) { continue; } if (is_array($value)) { $params[] = audio_query_string_encode($value, $exclude, $key); } else { $params[] = $key .'='. urlencode($value); } } return implode('&', $params); } /** * Determines if an audio node can be played by Flash players. * * @param $node * Node object. * @return * Boolean */ function audio_is_flash_playable($node) { // Flash only supports a limited range of sample rates. switch ($node->audio_file['sample_rate']) { case '44100': case '22050': case '11025': return TRUE; default: return FALSE; } } /** * Implementation of the token.module's hook_token_values(). * * @param $type * The current context -- 'node', 'user', 'global', etc. * @param $object * The specific node, user, etc. that should be used as the basis for the * replacements. * @return * Array of tokens. */ function audio_token_values($type, $object = NULL) { $node = $object; if ($type == 'node' && $node->type == 'audio') { // Tags. foreach (audio_get_tags_allowed() as $tag) { if (isset($node->audio_tags[$tag])) { $name = strtr($tag, '_', '-'); $tokens['audio-tag-'. $name] = check_plain($node->audio_tags[$tag]); $tokens['audio-tag-'. $name .'-raw'] = $node->audio_tags[$tag]; } } // Formatted file info. $tokens['audio-length'] = theme('audio_format_filelength', $node->audio_file); $tokens['audio-format'] = theme('audio_format_fileformat', $node->audio_file); // Raw file info. $keys = array('file_format', 'file_name', 'file_path', 'file_mime', 'file_size', 'sample_rate', 'channel_mode', 'bitrate', 'bitrate_mode', 'playtime'); foreach ($keys as $key) { if (isset($node->audio_file[$key])) { $tokens['audio-'. strtr($key, '_', '-')] = check_plain($node->audio_file[$key]); } } // Play and download links. if (isset($node->url_play)) { $tokens['audio-player'] = audio_get_node_player($node); $tokens['audio-url-play'] = $node->url_play; } if (isset($node->url_download)) { $tokens['audio-url-download'] = $node->url_download; } return $tokens; } } /** * Implementation of the token.module's hook_token_values(). * * @param $type * Indicates the context that token help is being generated for. Unlike * hook_token_values however, you should show ALL tokens at the same time if * $type is 'all'. * @return * Array of token information. */ function audio_token_list($type = 'all') { if ($type == 'node' || $type == 'all') { // Tags. foreach (audio_get_tags_allowed() as $tag) { $name = strtr($tag, '_', '-'); $tokens['node']['audio-tag-'. $name] = t("Audio node @tag tag value.", array('@tag' => $tag)); $tokens['node']['audio-tag-'. $name .'-raw'] = t("Audio node @tag tag value. WARNING - raw user input.", array('@tag' => $tag)); } // Formatted file info. $tokens['node']['audio-length'] = t("Audio node formatted file length information."); $tokens['node']['audio-format'] = t("Audio node formatted file format information."); // Raw file info. $tokens['node']['audio-sample-rate'] = t("Audio node sample rate, integer e.g. 44100."); $tokens['node']['audio-channel-mode'] = t("Audio node channels, e.g. mono, stereo."); $tokens['node']['audio-bitrate'] = t("Audio node bitrate, integer e.g. 19200."); $tokens['node']['audio-bitrate-mode'] = t("Audio node bitrate encoding mode, e.g. vbr, cbr."); $tokens['node']['audio-playtime'] = t("Audio node play time, minutes:seconds."); $tokens['node']['audio-file-format'] = t("Audio node file format, e.g. mp3, ogg"); $tokens['node']['audio-file-name'] = t("Audio node original, uploaded file name."); $tokens['node']['audio-file-path'] = t("Audio node file path."); $tokens['node']['audio-file-mime'] = t("Audio node MIME type."); $tokens['node']['audio-file-size'] = t("Audio node file size, in bytes."); // Play and download links. $tokens['node']['audio-player'] = t("Audio node player."); $tokens['node']['audio-url-play'] = t("Audio node play URL."); $tokens['node']['audio-url-download'] = t("Audio node download URL."); return $tokens; } }