'. 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:
'. 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.'. 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', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => '-10', ); $items['admin/settings/audio/metadata'] = array( 'title' => 'Metadata tags', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings_metadata'), 'access arguments' => array('administer audio'), 'file' => 'audio.admin.inc', 'type' => MENU_LOCAL_TASK, ); $items['admin/settings/audio/players'] = array( 'title' => 'Players', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings_players'), 'access arguments' => array('administer audio'), 'file' => 'audio.admin.inc', 'type' => MENU_LOCAL_TASK, ); $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', 'file' => 'audio.pages.inc', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['audio/by/%'] = array( 'title' => 'Audio by @tag', 'title arguments' => array('@tag' => 2), 'page callback' => 'audio_page_browse_by', 'page arguments' => array(2), 'file' => 'audio.pages.inc', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['audio/by/%/%'] = array( 'title' => 'Audio by @tag @value', 'title arguments' => array('@tag' => 2, '@value' => 3), 'page callback' => 'audio_page_browse_by', 'page arguments' => array(2, 3), 'file' => 'audio.pages.inc', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['audio/download/%node'] = array( 'page callback' => 'audio_download', 'page arguments' => array(2), 'file' => 'audio.pages.inc', 'access callback' => '_audio_allow_download', 'access arguments' => array(2), 'type' => MENU_CALLBACK, ); $items['audio/play/%node'] = array( 'page callback' => 'audio_play', 'page arguments' => array(2), 'file' => 'audio.pages.inc', 'access callback' => '_audio_allow_play', 'access arguments' => array(2), 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_theme */ function audio_theme() { $theme = array( 'audio_file_form' => array( 'arguments' => array('form'), ), 'audio_admin_settings_metadata_table' => 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'), 'file' => 'audio.theme.inc', ), 'audio_display' => array( 'arguments' => array('node'), 'file' => 'audio.theme.inc', ), 'audio_format_tag' => array( 'arguments' => array('tag', 'value', 'setting'), 'file' => 'audio.theme.inc', ), 'audio_format_filelength' => array( 'arguments' => array('fileinfo'), 'file' => 'audio.theme.inc', ), 'audio_format_fileformat' => array( 'arguments' => array('fileinfo'), 'file' => 'audio.theme.inc', ), ); $player_path = drupal_get_path('module', 'audio') .'/players'; foreach (audio_get_players('names') as $name => $player) { if ($player['module'] == 'audio') { $theme[$player['theme_node']] = array( 'arguments' => array('node', 'options'), 'file' => $player['file'], 'path' => $player_path, ); if (!empty($player['theme_xspf'])) { $theme[$player['theme_xspf']] = array( 'arguments' => array('path', 'options'), 'file' => $player['file'], 'path' => $player_path, ); } } } return $theme; } /** * 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: * '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 ($node->type == 'audio' && isset($node->url_download) && $node->audio['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 ($node->type == 'audio' && 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['download_count'])), ); } } if (_audio_allow_play($node) && $link_access) { $links['audio_play_count'] = array( 'title' => t('@play_count plays', array('@play_count' => $node->audio['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', 'value' => '', 'attributes' => array( 'url' => $node->url_download, 'length' => $node->audio['file']->filesize, 'type' => $node->audio['file']->filemime )); // Provide very basic iTunes support. $ret[] = array( 'namespace' => array('xmlns:itunes' => 'http://www.itunes.com/dtds/podcast-1.0.dtd'), 'key' => 'itunes:duration', 'value' => $node->audio['playtime'], ); $ret[] = array( 'namespace' => array('xmlns:itunes' => 'http://www.itunes.com/dtds/podcast-1.0.dtd'), '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'] = array( '#value' => theme('audio_teaser', $node), '#weight' => 0, ); } else { $node->content['audio'] = array( '#value' => theme('audio_display', $node), '#weight' => -1, ); } return $node; } /** * Implementation of hook_validate(). */ function audio_validate(&$node, &$form) { if (!isset($node->audio['file']->filepath)) { 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.")); } if (empty($form_state['values']['title_format'])) { $form_state['values']['title_format'] = variable_get('audio_default_title_format', '[audio-tag-title-raw] by [audio-tag-artist-raw]'); } } /** * 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 a.* FROM {audio} a WHERE vid=%d", $node->vid)); $file = db_fetch_object(db_query("SELECT * FROM {files} WHERE fid=%d", $fields['fid'])); $ret = array( 'title_format' => $fields['title_format'], 'audio' => array( 'play_count' => $fields['play_count'], 'download_count' => $fields['download_count'], 'downloadable' => $fields['downloadable'], 'format' => $fields['format'], 'sample_rate' => $fields['sample_rate'], 'channel_mode' => $fields['channel_mode'], 'bitrate' => $fields['bitrate'], 'bitrate_mode' => $fields['bitrate_mode'], 'playtime' => $fields['playtime'], 'bitrate' => $fields['bitrate'], 'file' => $file, ), ); if (isset($file->filepath) && file_exists($file->filepath)) { // TODO: should these links be by vid? $ret['url_play'] = url('audio/play/'. $node->nid, array('absolute' => TRUE)); if ($ret['audio']['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 .'/'. $file->filename; $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; } return $ret; } return array(); } /** * Implementation of hook_insert(). */ function audio_insert(&$node) { file_move($node->audio['file']->filepath, audio_get_directory(), FILE_EXISTS_RENAME); _audio_save($node); } /** * Implementation of hook_update(). */ function audio_update(&$node) { // If there's a new file we need to move it to the correct location. if (($node->audio['file']->status & FILE_STATUS_PERMANENT) != FILE_STATUS_PERMANENT) { if (!$node->revision) { // If it's not a revision the existing file needs to be removed. $oldfile = db_fetch_object(db_query('SELECT f.* FROM {files} f INNER JOIN {audio} a ON f.fid = a.fid WHERE a.vid = %d', $node->vid)); _audio_file_delete($oldfile); } file_move($node->audio['file']->filepath, audio_get_directory(), FILE_EXISTS_RENAME); } else if ($node->revision) { // New revision using existing file so we need to copy it. file_copy($node->audio['file']->filepath, file_create_filename($node->audio['file']->filename, audio_get_directory())); // Unset the fid so a new record is created. $node->audio['file']->fid = NULL; } _audio_save($node); } /** * Handle the busy work of saving the node's audio and file records. * * @param $node Node opject. */ function _audio_save(&$node) { // Save the file $node->audio['file']->timestamp = time(); $node->audio['file']->filesize = filesize($node->audio['file']->filepath); $node->audio['file']->status |= FILE_STATUS_PERMANENT; // If there's an fid update, otherwise insert. $file_update = empty($node->audio['file']->fid) ? array() : array('fid'); drupal_write_record('files', $node->audio['file'], empty($node->audio['file']->fid) ? array() : array('fid')); // Save the audio row. $audio = array_merge($node->audio, array('nid' => $node->nid, 'vid' => $node->vid, 'title_format' => $node->title_format, 'fid' => $node->audio['file']->fid)); $audio_update = ($node->is_new || $node->revision) ? array() : array('vid'); drupal_write_record('audio', $audio, $audio_update); // Remove any existing metadata. db_query("DELETE FROM {audio_metadata} WHERE vid=%d", $node->vid); // Save the new tags. $allowed_tags = audio_get_tags_allowed(); foreach ($node->audio_tags as $tag => $value) { if (in_array($tag, $allowed_tags) && $value) { $metadata = array('vid' => $node->vid, 'tag' => $tag, 'value' => $value, 'clean' => audio_clean_tag($value)); drupal_write_record('audio_metadata', $metadata); } } } /** * Implementation of hook_delete(). */ function audio_delete($node) { $result = db_query('SELECT a.vid, f.* FROM {audio} a LEFT JOIN {files} f ON a.fid = f.fid WHERE nid = %d', $node->nid); while ($o = db_fetch_object($result)) { _audio_file_delete($o); 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) { $file = db_result(db_query('SELECT a.vid, f.* FROM {audio} a LEFT JOIN {files} f ON a.fid = f.fid WHERE vid = %d', $node->vid)); _audio_file_delete($file); db_query('DELETE FROM {audio_metadata} WHERE vid = %d', $node->vid); db_query('DELETE FROM {audio} WHERE vid = %d', $node->vid); } /** * Handle the busy work of deleting the file record and the file. * * @param unknown_type $file */ function _audio_file_delete($file) { if (!empty($file->filepath)) { file_delete($file->filepath); } if (!empty($file->fid)) { db_query('DELETE FROM {files} WHERE fid = %d', array($file->fid)); } } /** * Implementation of hook_form(). */ function audio_form(&$node, &$form_state) { drupal_add_js(drupal_get_path('module', 'audio') .'/audio.js'); $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['audio'] = array( '#type' => 'fieldset', '#title' => t('Audio File Info'), '#collapsible' => TRUE, '#weight' => -1, '#tree' => TRUE, ); // Place a visible copy of the file path on the form (after removing the // directory info from non-admins). $form['audio']['display_filepath'] = array( '#type' => 'item', '#title' => t('Current File'), '#value' => empty($node->audio['file']->filepath) ? t('No file is attached.') : (user_access('administer audio') ? $node->audio['file']->filepath : basename($node->audio['file']->filepath)), ); // If we've got a file, add the file information fields. if (!empty($node->audio['file']->filename)) { $form['audio']['#theme'] = 'audio_file_form'; // Store the non-user editable file information as values. $form['audio']['file'] = array( '#type' => 'value', '#value' => $node->audio['file'], ); $form['audio']['format'] = array( '#type' => 'select', '#title' => t('Format'), '#default_value' => $node->audio['format'], '#options' => drupal_map_assoc(array('', 'aac', 'ac3', 'au', 'avr', 'flac', 'midi', 'mod', 'mp3', 'mpc', 'ogg', 'voc'), 'drupal_strtoupper'), ); $form['audio']['file_size'] = array( '#type' => 'textfield', '#title' => t('File Size'), '#default_value' => $node->audio['file']->filesize, ); $form['audio']['playtime'] = array( '#type' => 'textfield', '#title' => t('Length'), '#default_value' => $node->audio['playtime'], '#description' => t('The format is hours:minutes:seconds.'), ); $form['audio']['sample_rate'] = array( '#type' => 'select', '#title' => t('Sample rate'), '#default_value' => $node->audio['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']['channel_mode'] = array( '#type' => 'select', '#title' => t('Channel mode'), '#default_value' => $node->audio['channel_mode'], '#options' => array('stereo' => t('Stereo'), 'mono' => t('Mono')), ); $form['audio']['bitrate'] = array( '#type' => 'textfield', '#title' => t('Bitrate'), '#default_value' => $node->audio['bitrate'], ); $form['audio']['bitrate_mode'] = array( '#type' => 'select', '#title' => t('Bitrate mode'), '#default_value' => $node->audio['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']['display_play_count'] = array( '#type' => 'item', '#title' => t('Play count'), '#value' => $node->audio['play_count'], ); $form['audio']['display_download_count'] = array( '#type' => 'item', '#title' => t('Download count'), '#value' => $node->audio['download_count'], ); // ...and these are what we'll save back to the node. $form['audio']['play_count'] = array( '#type' => 'value', '#value' => $node->audio['play_count'], ); $form['audio']['download_count'] = array( '#type' => 'value', '#value' => $node->audio['download_count'], ); } $extensions = implode(', ', array_filter(explode(' ', variable_get('audio_allowed_extensions', 'mp3 wav ogg')))); $form['audio']['audio_upload'] = array( '#tree' => FALSE, '#type' => 'file', '#title' => empty($node->audio['file']->filename) ? t('Add a new audio file') : t('Replace this with a new file'), '#attributes' => array('audio_accept' => $extensions), '#description' => t('Click "Browse..." to select an audio file to upload. Only files with the following extensions are allowed: %allowed-extensions.', array('%allowed-extensions' => $extensions)) .'