/[drupal]/contributions/sandbox/jamesandres/audio_external/audio_external.module
ViewVC logotype

Contents of /contributions/sandbox/jamesandres/audio_external/audio_external.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (show annotations) (download) (as text)
Tue Sep 11 23:03:09 2007 UTC (2 years, 2 months ago) by jamesandres
Branch: MAIN
CVS Tags: HEAD
File MIME type: text/x-php
*** empty log message ***
1 <?php
2 /**
3 * @file audio_external.module
4 * Adds the ability to use external MP3's with the stock audio module.
5 *
6 * @author James Andres
7 * @since 2007-09-11
8 **/
9
10 /**
11 * Implementation of hook_help().
12 */
13 function audio_external_help($section) {
14 switch ($section) {
15 case 'admin/help#audio_external':
16 return t('Allows external files with the audio module.');
17 }
18 }
19
20 /**
21 * Implementation of hook_form_alter()
22 **/
23 function audio_external_form_alter($form_id, &$form) {
24 if ($form_id == 'audio_node_form') {
25 unset($form['#post']['audio_tags']);
26
27 $form['audio_fileinfo']['remote_markup'] = array(
28 '#weight' => 19.1,
29 '#value' => '<div style="text-align: center;"> or </div>',
30 );
31 $form['audio_fileinfo']['remote_file'] = array(
32 '#type' => 'textfield',
33 '#title' => t('Link to remote file'),
34 '#description' => t('Use this field to link to a remote MP3 file. This approach may take more time to process after clicking submit.'),
35 '#default_value' => $form['#node']->remote_file,
36 '#weight' => 19.2,
37 '#required' => FALSE,
38 );
39 }
40 }
41
42 /**
43 * Implementation of hook_audio()
44 **/
45 function audio_external_audio($op, &$node, $a3 = NULL, $a4 = NULL) {
46 $remote_file = $_POST['audio_fileinfo']['remote_file'];
47
48 switch ($op) {
49 // NOTE: This works because upload always gets called before prepare.
50 case 'upload':
51 $node->new_audio_uploded = TRUE;
52 break;
53
54 case 'prepare':
55 _audio_external_prepare($remote_file, $node, TRUE);
56 break;
57
58 case 'load':
59 $file = db_fetch_object(db_query(
60 'SELECT * FROM {audio_file} WHERE vid = %d',
61 $node->vid
62 ));
63
64 if (!file_exists($file->filepath) && url_exists($file->filepath)) {
65 return array(
66 'url_play' => $file->filepath,
67 'url_download' => $file->filepath,
68 'remote_file' => $file->filepath,
69 );
70 }
71 break;
72 }
73 }
74
75 function _audio_external_prepare($remote_file, &$node, $trixy = FALSE) {
76 if ($remote_file && !$node->new_audio_uploded && ($attributes = curl_get_attributes($remote_file))) {
77 if ($attributes['filemime'] != 'audio/mpeg') {
78 drupal_set_message(t('Only remote MP3 files are supported at this time! Other file types may be supported in the future.'), 'error');
79 return FALSE;
80 }
81
82 // TODO: Use _audio_external_get_remote_id3() here!
83 $cache_file = _audio_external_get_cached_file($remote_file);
84
85 // TODO: file_exists isn't the best way to do this, there should be a time limit on how long a cached file can last for..
86 if (!file_exists($cache_file) && $attributes['filesize']) {
87 // TODO: Currently this function isn't smart enough to know how
88 // to gracefully disengage from a server that doesn't support
89 // the HTTP range header.
90 transfer_id3_header_and_footer($remote_file, $cache_file, $attributes['filesize']);
91 } else if (!$attributes['filesize']) {
92 drupal_set_message(t('Could not retrieve file size from remote file!'), 'error');
93 return FALSE;
94 }
95
96 $node->audio_file = (object) array(
97 'origname' => $attributes['filename'],
98 'filename' => $attributes['filename'],
99 'filepath' => $cache_file,
100 'filemime' => $attributes['filemime'],
101 'filesize' => $attributes['filesize'],
102 );
103 $node->audio_fileinfo = array();
104
105 $node->url_play = $remote_file;
106 $node->url_download = $remote_file;
107 $node->remote_file = $remote_file;
108
109 if (module_exists('audio_getid3')) {
110 module_invoke('audio_getid3', 'audio', 'upload', $node);
111 }
112
113 $defaults = audio_get_tag_defaults();
114 foreach (audio_get_tag_settings() as $tag => $settings) {
115 if (!$node->audio_tags[$tag]) {
116 $node->audio_tags[$tag] = $defaults[$tag];
117 }
118 }
119
120 if ($trixy) {
121 //Lastly, "trick" the audio module into thinking a file was uploaded
122 $nid = ($node->nid) ? $node->nid : 'new_node';
123 if (!$_SESSION['audio_file'][$nid]) {
124 // $node->audio_file['filepath'] = $remote_file;
125 $_SESSION['audio_file'][$nid] = $node->audio_file;
126 }
127 }
128
129 return TRUE;
130 }
131
132 return FALSE;
133 }
134
135 /**
136 * Get remote ID3 information.
137 **/
138 function _audio_external_get_remote_id3($remote_file, $quiet = FALSE) {
139 $cache_file = _audio_external_get_cached_file($remote_file);
140
141 if (!file_exists($cache_file) && $attributes['filesize']) {
142 // If data wasn't transfered make an error.
143 if (transfer_id3_header_and_footer($remote_file, $cache_file, $attributes['filesize']) <= 0) {
144 if (!$quiet) {
145 drupal_set_message(t('Could not get header and footer of remote file!'), 'error');
146 }
147
148 return FALSE;
149 }
150 } else if (!$attributes['filesize']) {
151 if (!$quiet) {
152 drupal_set_message(t('Could not retrieve file size from remote file!'), 'error');
153 }
154
155 return FALSE;
156 }
157
158 // TODO: audio_read_id3tags() throws errors and warnings sometimes, this isn't desierable.
159 if ($metadata = audio_read_id3tags($cache_file)) {
160 return $metadata;
161 }
162
163 return FALSE;
164 }
165
166 /**
167 * Implementation of hook_nodeapi()
168 *
169 * Need to implement a few nodeapi operations since they run at different
170 * times than audioapi. Often after, which is necessary for insert and
171 * update.
172 **/
173 function audio_external_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
174 if ($node->type != 'audio') {
175 return;
176 }
177
178 switch ($op) {
179 case 'insert':
180 case 'update':
181 // Before this hook was called the cached copy of the file was already
182 // moved into the files/audio/.. directory. This isn't really desierable
183 // but it can't be helped. Here we will undo some of this mess, and
184 // redo a few of the database queries.
185
186 if ($node->audio_fileinfo['remote_file'] && ($attributes = curl_get_attributes($node->audio_fileinfo['remote_file']))) {
187 // If the cached file did get moved in locally move it back out.
188 // This code should always run, in theory.
189 if (file_exists($node->audio_file->filepath) && ($cache_file = _audio_external_get_cached_file($node->audio_fileinfo['remote_file']))) {
190 // Move the file back, for optimization purposes.
191 rename($node->audio_file->filepath, $cache_file);
192 }
193
194 db_query(
195 "UPDATE {audio_file} SET origname = '%s', filename = '%s', filepath = '%s', filemime = '%s', filesize = %d WHERE vid = %d",
196 $attributes['filename'],
197 $attributes['filename'],
198 $node->audio_fileinfo['remote_file'],
199 $attributes['filemime'],
200 $attributes['filesize'],
201 $node->vid
202 );
203
204 // cache_clear_all();
205 }
206 break;
207 }
208 }
209
210 function audio_get_tag_defaults() {
211 // Some of these are rather pathetic defaults, I know.
212 $defaults = array(
213 'artist' => 'Unknown Artist',
214 'title' => 'Unknown Title',
215 'album' => 'Unknown Album',
216 'track' => '1',
217 'genre' => 'Unknown',
218 'year' => date('Y'),
219 );
220
221 return variable_get('audio_tag_defaults', $defaults);
222 }
223
224 function _audio_external_get_cached_file($remote_file) {
225 // NOTE: This is a tad insecure, maybe choose a more secure default to store the caches?
226 $basepath = variable_get('audio_external_base_path', file_create_path() . '/audio_external_cache');
227
228 // Try to create the cache path, this should only run once.
229 if (!is_dir($basepath) && ! @mkdir($basepath, 0755, 1)) {
230 drupal_set_message(t('Could not create audio_external cache path'), 'error');
231
232 return FALSE;
233 }
234
235 // TODO: Properly find the extension here.
236 return $basepath . '/' . md5($remote_file) . ($ext ? $ext : '.mp3');
237 }
238
239 /**
240 * Get the size of a remote file via cURL.
241 *
242 * @param $url
243 * The remote file.
244 * @return (mixed)
245 * The size of the file, or boolean FALSE if indeterminate.
246 **/
247 function curl_get_attributes($url, $wipe = FALSE) {
248 $cache = cache_get('curl_get_attributes_cache', 'cache');
249
250 $curl_remote_attributes = array();
251 if ($cache->data && !$wipe) {
252 $curl_remote_attributes = unserialize($cache->data);
253 }
254
255 if ($curl_remote_attributes[$url]['expiry'] > time() && !$wipe) {
256 return $curl_remote_attributes[$url];
257 }
258
259 try {
260 $ch = curl_init($url);
261
262 // Use output buffering to grab cURL output.
263 ob_start();
264 curl_setopt($ch, CURLOPT_HEADER, 1);
265 curl_setopt($ch, CURLOPT_NOBODY, 1);
266 curl_setopt($ch, CURLOPT_TIMEOUT, CURL_DEFAULT_TIMEOUT);
267
268 // Let's be IE6
269 curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)');
270 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
271 $ok = curl_exec($ch);
272
273 $head = ob_get_contents();
274 ob_end_clean();
275
276 // File size
277 $regex = '/Content-Length:[\s]*([0-9]+)/';
278 preg_match($regex, $head, $matches);
279 $results['filesize'] = isset($matches[1]) ? (int) $matches[1] : NULL;
280
281 // File name
282 $regex = '/Content-Disposition: attachment; filename=("([^"]*)"|\'([^\']*)\');/i';
283 preg_match($regex, $head, $matches);
284 // $matches[1] is the outside match, like '123.mp3' or "123.mp3". The substr strips off the first and last char (ie: the quotes).
285 $results['filename'] = isset($matches[1]) ? substr($matches[1], 1, count($matches[1]) - 2) : NULL;
286 if ($results['filename'] === NULL) {
287 $url_parts = parse_url($url);
288 $results['filename'] = basename($url_parts['path']);
289 }
290
291 // File mime
292 $regex = '/Content-Type: ([0-9A-Za-z-.\/]+)/';
293 preg_match($regex, $head, $matches);
294 $results['filemime'] = isset($matches[1]) ? $matches[1] : NULL;
295 if (!$results['filemime'] && $results['filename'] && function_exists('mime_content_type')) {
296 $results['filemime'] = mime_content_type($results['filename']);
297 }
298
299 curl_close($ch);
300 } catch (Exception $e) {
301 @curl_close($ch);
302 watchdog('audio_external', 'Error getting file for "' . $url . '" size using cURL: "' . $e->getMessage() . '".');
303 return FALSE;
304 }
305
306 // Cache results
307 $curl_remote_attributes[$url] = $results;
308 $curl_remote_attributes[$url]['expiry'] = variable_get('curl_get_attributes_expiry', strtotime("+30 minutes"));
309
310 cache_set('curl_get_attributes_cache', 'cache', serialize($curl_remote_attributes));
311
312 return $results;
313 }
314
315 /**
316 * Transfer the first 64KB and the last 1KB of a file into a temporary location
317 *
318 * NOTE: Some servers don't properly support the HTTP range header. Generally
319 * those servers just blindly send us the whole file. There is no easy
320 * way to deal with this problem.
321 * NOTE: This function relies heavily on cURL. It probably shouldn't.
322 *
323 * @author James Andres
324 *
325 * @param $url
326 * A URL for cURL to point at.
327 * @param $file
328 * The temporary file you want to save the data into.
329 * @param $size
330 * The size of the remote file.
331 * @return (int) how many bytes were grabbed. If this is non-zero you can
332 * fairly safely assume success.
333 **/
334 function transfer_id3_header_and_footer($url, $file, $size = 0, $to_get = 65536, $from_end = 1024) {
335 try {
336 $fudge_factor = 32; // No worries mate, a few bytes off .. it's still okay!
337
338 // Grab the head of the file (first 64 KB)
339 $ch = curl_init($url);
340 curl_setopt($ch, CURLOPT_RANGE, "0-$to_get");
341 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
342 curl_setopt($ch, CURLOPT_HEADER, FALSE);
343 curl_setopt($ch, CURLOPT_TIMEOUT, CURL_DEFAULT_TIMEOUT);
344 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
345 // Let's be IE6
346 curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)');
347 $data = curl_exec($ch);
348 curl_close($ch);
349
350 // If we DO know the file size and the server DID properly respect the RANGE
351 // header continue to grab the tail of the file.
352 if ($size > $to_get && strlen($data) < ($to_get + $fudge_factor)) {
353 // Grab the tail of the file, last 1 KB.
354 $tail = ($size - $from_end) . '-' . $size;
355 $ch = curl_init($url);
356 curl_setopt($ch, CURLOPT_RANGE, (string) $tail);
357 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
358 curl_setopt($ch, CURLOPT_HEADER, FALSE);
359 curl_setopt($ch, CURLOPT_TIMEOUT, CURL_DEFAULT_TIMEOUT);
360 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
361 // Let's be IE6
362 curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)');
363 $data .= curl_exec($ch);
364 curl_close($ch);
365 }
366
367 file_put_contents($file, $data);
368 $downloaded_size = filesize($file);
369 } catch (Exception $e) {
370 @curl_close($ch);
371 watchdog('audio_external', 'Error transfering file using cURL: "' . $e->getMessage() . '".');
372 return 0;
373 }
374
375 return $downloaded_size;
376 }
377
378 /**
379 * From http://ca3.php.net/manual/en/function.file-exists.php#59986
380 *
381 * Added caching support for better optimization.
382 **/
383 function url_exists($url) {
384 $cache = cache_get('url_exists_cache', 'cache');
385
386 if ($cache->data && ($cached_urls = unserialize($cache->data)) && $cached_urls[$url]['expiry'] > time()) {
387 return $cached_urls[$url]['exists'];
388 } else {
389 $cached_urls = array();
390 }
391
392 $a_url = parse_url($url);
393 if (!isset($a_url['port'])) $a_url['port'] = 80;
394 $errno = 0;
395 $errstr = '';
396 $timeout = 5;
397 if(isset($a_url['host']) && $a_url['host']!=gethostbyname($a_url['host'])) {
398 $fid = @fsockopen($a_url['host'], $a_url['port'], $errno, $errstr, $timeout);
399 if (!$fid) $result = FALSE;
400 $page = isset($a_url['path']) ?$a_url['path']:'';
401 $page .= isset($a_url['query'])?'?'.$a_url['query']:'';
402 fputs($fid, 'HEAD '.$page.' HTTP/1.0'."\r\n".'Host: '.$a_url['host']."\r\n\r\n");
403 $head = fread($fid, 4096);
404 fclose($fid);
405 $result = (boolean) preg_match('#^HTTP/.*\s+[200|302]+\s#i', $head);
406 } else {
407 $result = FALSE;
408 }
409
410 $cached_urls[$url] = array(
411 'exists' => $result,
412 'expiry' => variable_get('url_exists_cache_expire', strtotime("+3 hours")),
413 );
414
415 cache_set('url_exists_cache', 'cache', serialize($cached_urls));
416
417 return $result;
418 }
419
420 /**
421 * Function for other modules to use to create an audio node using an external
422 * file.
423 *
424 * Unfortunately the audio module's API method is too stuck in it's ways to
425 * modify (only the upload op is fired). That's what makes this function
426 * required.
427 *
428 * @param $remote_file
429 * full URL path to the MP3 in question.
430 * @param $title_format
431 * a t() formatting string for generating the node's title. you can use any
432 * value in the node's audio_tags array as variable. if nothing is provided
433 * the default title format will be used.
434 * @param $tags
435 * an, optional, array of metadata to add to the node. these will be
436 * available to the $title_format.
437 * @return
438 * a node or FALSE on error
439 */
440 function audio_api_insert_remote_file($remote_file, $title_format = NULL, $tags = array()) {
441 global $user;
442
443 //check for user permission...
444 if (!audio_access('create')) {
445 drupal_access_denied();
446 }
447
448 // start building a node
449 $node = new stdClass();
450 $node->type = 'audio';
451 $node->uid = $user->uid;
452 $node->name = $user->name;
453
454 // set the node's defaults... (copied this from node and comment.module)
455 $node_options = variable_get('node_options_'. $node->type, array('status', 'promote'));
456 $node->status = in_array('status', $node_options);
457 $node->moderate = in_array('moderate', $node_options);
458 $node->promote = in_array('promote', $node_options);
459 if (module_exists('comment')) {
460 $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
461 }
462
463 // default audio info
464 $node->title_format = $title_format;
465 $node->audio_tags = array();
466 $node->audio_images = array();
467
468 // file object
469 $node->audio_file = new stdClass();
470 $node->audio_file->newfile = TRUE;
471
472 _audio_external_prepare($remote_file, $node);
473
474 // fileinfo
475 $node->audio_fileinfo = array(
476 'downloadable' => variable_get('audio_default_downloadable', 1),
477 'play_count' => 0,
478 'download_count' => 0,
479 'filesize' => $node->audio_file->filesize,
480 'remote_file' => $remote_file,
481 );
482
483 // allow other modules to modify the node (hopefully reading in tags)
484 audio_invoke_audioapi('upload', $node);
485
486 // add the tags (overwriting any that audio_getid3 loaded)
487 if ($tags) {
488 $node->audio_tags = $tags;
489 }
490
491 // ...then save it
492 $node->title = audio_build_title($node);
493 $node = node_submit($node);
494 node_save($node);
495
496 return $node;
497 }
498
499 /**
500 * Check if a URL is already in the system, no point in re-importing tracks.
501 **/
502 function audio_external_is_duplicate($url) {
503 $attributes = curl_get_attributes($url);
504
505 // Match if the same URL is found in the system elsewhere--AKA "the easy case."
506 $same_url_sql = "SELECT nid FROM {song_files} WHERE filepath = '%s'";
507 $vid = db_result(db_query(
508 "SELECT vid FROM {audio_file} WHERE filepath = '%s'", $url
509 ));
510
511 if ($vid) {
512 return node_load(array('vid' => $vid));
513 }
514
515 $metadata = _audio_external_get_remote_id3($remote_file);
516
517 // NOTE: This function is from projectopus.com, the lengths part doesn't apply here.
518 // // Get a "cloud" of lengths around the song length
519 // // (ie: 05:01, 05:02, 05:03, 05:04, 05:05, *05:06*, 05:07, 05:08, 05:09 .. etc.)
520 // $length_seconds = _get_length_as_seconds($metadata['fileinfo']['playtime']);
521 // if ($length_seconds > 5) {
522 // for ($i = $length_seconds - 5; $i <= $length_seconds + 5; $i++) {
523 // $lengths[] = _get_playtime_string($i);
524 // }
525 // $lengths = implode("', '", $lengths);
526 // } else {
527 // $lengths = $song_file->length;
528 // }
529 // // Do this ONLY for known lengths.
530 // if (!strpos($lengths, '?:?')) {
531 // $lengths_sql = "OR s.length IN('" . db_escape_string($lengths) . "')";
532 // }
533
534 $fudge_factor = 1024; // Fudge factor of 1024 bytes
535 $filesize_lower = ($attributes['filesize'] - $fudge_factor);
536 $filesize_lower = ($filesize_lower > 0 ? $filesize_lower : 0);
537 $filesize_upper = ($attributes['filesize'] + $fudge_factor);
538
539 // Match if the filename's are the same and the playtime or filesize are within tolerance.
540 $sql =<<<SQL
541 SELECT vid
542 FROM {audio_file}
543 WHERE LOWER(filename) = '%s' AND (filesize > %d AND filesize < %d)
544 SQL;
545 $vid = db_result(db_query(
546 $sql,
547 strtolower($filename), $filesize_lower, $filesize_upper
548 ));
549 if ($vid) {
550 return node_load(array('vid' => $vid));
551 }
552
553 // Match if the artist and title are the same and the playtime or filesize are within tolerance.
554 $sql =<<<SQL
555 SELECT vid FROM {audio_file}
556 WHERE (filesize > %d AND filesize < %d)
557 AND EXISTS (SELECT vid FROM {audio_metadata} WHERE tag = 'title' AND LOWER(value) = '%s')
558 AND EXISTS (SELECT vid FROM {audio_metadata} WHERE tag = 'artist' AND LOWER(value) = '%s')
559 SQL;
560
561 $vid = db_result(db_query(
562 $sql,
563 $filesize_lower, $filesize_upper,
564 strtolower($metadata['tags']['title']),
565 strtolower($metadata['tags']['artist'])
566 ));
567 if ($vid) {
568 return node_load(array('vid' => $vid));
569 }
570
571 return FALSE;
572 }
573
574 function _get_playtime_string($seconds) {
575 // Add 3600 to the units to add hours!
576 $units = array(60, 1); $parts = array();
577 if ($seconds >= 3600) {
578 $units = array(3600, 60, 1);
579 }
580
581 foreach ($units as $unit) {
582 if ($seconds >= $unit) {
583 $part = floor($seconds / $unit);
584 $seconds -= $part * $unit;
585 } else {
586 $part = 0;
587 }
588 $parts[] = sprintf("%02d", $part);
589 }
590
591 return implode(':', $parts);
592 }
593
594 /**
595 * Converts a length given as a string (hh:mm:ss) to seconds.
596 *
597 * NOTE: Rewritten by James Andres; August 24th, 2006. Added $in_milliseconds
598 * param.
599 * NOTE: I kept the 'return -1 when over maximum' logic intact although I'm
600 * not sure if this is necessary.
601 *
602 * @param string $length_string
603 * The length in string format hh:mm:ss
604 * @param boolean $in_milliseconds
605 * Return the result in milliseconds?
606 * @return int $secs
607 * The length in seconds
608 */
609 function _get_length_as_seconds($length_string, $in_milliseconds = false) {
610 // Handle "unknown lengths" for offsite hosted music.
611 if (strpos($length_string, '?:?')) {
612 return 0;
613 }
614
615 // Bust up and reverse the array; ie: array(ss, mm, hh)
616 $parts = array_reverse(explode(':', $length_string));
617 // Time multipliers; ie: 1 second in a second, 60 sec in a min .. etc.
618 $multipliers = array(1, 60, 60 * 60);
619 // The maximum each unit can be (else an error will be generated)
620 $maximums = array(60, 60, 24);
621
622 foreach ($parts as $i => $part) {
623 if ($part <= $maximums[$i]) {
624 $secs += $multipliers[$i] * $part;
625 } else {
626 return -1;
627 }
628 }
629
630 if ($in_milliseconds) {
631 return $secs * 1000;
632 } else {
633 return $secs;
634 }
635 }

  ViewVC Help
Powered by ViewVC 1.1.2