Ampache PHP Cross Reference Groupware Applications

Source: /play/index.php - 515 lines - 17902 bytes - Summary - Text - Print

Description: LICENSE: GNU General Public License, version 2 (GPLv2) Copyright 2001 - 2014 Ampache.org This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License v2 as published by the Free Software Foundation.

   1  <?php
   2  /* vim:set softtabstop=4 shiftwidth=4 expandtab: */
   3  /**
   4   *
   5   * LICENSE: GNU General Public License, version 2 (GPLv2)
   6   * Copyright 2001 - 2014 Ampache.org
   7   *
   8   * This program is free software; you can redistribute it and/or
   9   * modify it under the terms of the GNU General Public License v2
  10   * as published by the Free Software Foundation.
  11   *
  12   * This program is distributed in the hope that it will be useful,
  13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15   * GNU General Public License for more details.
  16   *
  17   * You should have received a copy of the GNU General Public License
  18   * along with this program; if not, write to the Free Software
  19   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  20   *
  21   */
  22  
  23  /*
  24  
  25   This is the wrapper for opening music streams from this server.  This script
  26     will play the local version or redirect to the remote server if that be
  27     the case.  Also this will update local statistics for songs as well.
  28     This is also where it decides if you need to be downsampled.
  29  */
  30  define('NO_SESSION','1');
  31  require_once  '../lib/init.php';
  32  ob_end_clean();
  33  
  34  /* These parameters had better come in on the url. */
  35  $uid            = scrub_in($_REQUEST['uid']);
  36  $oid            = $_REQUEST['oid']
  37                  // FIXME: Any place that doesn't use oid should be fixed
  38                  ? scrub_in($_REQUEST['oid'])
  39                  : scrub_in($_REQUEST['song']);
  40  $sid            = scrub_in($_REQUEST['ssid']);
  41  $video          = make_bool($_REQUEST['video']);
  42  $type           = scrub_in($_REQUEST['type']);
  43  
  44  if (AmpConfig::get('transcode_player_customize')) {
  45      $transcode_to = scrub_in($_REQUEST['transcode_to']);
  46      $bitrate = intval($_REQUEST['bitrate']);
  47  } else {
  48      $transcode_to = null;
  49      $bitrate = 0;
  50  }
  51  $share_id       = scrub_in($_REQUEST['share_id']);
  52  
  53  if ($video) {
  54      // FIXME: Compatibility hack, should eventually be removed
  55      $type = 'video';
  56  }
  57  
  58  if (!$type) {
  59      // FIXME: Compatibility hack, should eventually be removed
  60      $type = 'song';
  61  }
  62  
  63  debug_event('play', 'Asked for type {'.$type."}", 5);
  64  
  65  if ($type == 'playlist') {
  66      $playlist_type = scrub_in($_REQUEST['playlist_type']);
  67      $oid = $sid;
  68  }
  69  
  70  /* This is specifically for tmp playlist requests */
  71  $demo_id    = Dba::escape($_REQUEST['demo_id']);
  72  $random     = Dba::escape($_REQUEST['random']);
  73  
  74  /* First things first, if we don't have a uid/oid stop here */
  75  if (empty($oid) && empty($demo_id) && empty($random)) {
  76      debug_event('play', 'No object UID specified, nothing to play', 2);
  77      header('HTTP/1.1 400 Nothing To Play');
  78      exit;
  79  }
  80  
  81  if (empty($uid)) {
  82      debug_event('play', 'No user specified', 2);
  83      header('HTTP/1.1 400 No User Specified');
  84      exit;
  85  }
  86  
  87  if (empty($share_id)) {
  88      $GLOBALS['user'] = new User($uid);
  89      Preference::init();
  90  
  91      /* If the user has been disabled (true value) */
  92      if (make_bool($GLOBALS['user']->disabled)) {
  93          debug_event('UI::access_denied', "$user->username is currently disabled, stream access denied",'3');
  94          header('HTTP/1.1 403 User Disabled');
  95          exit;
  96      }
  97  
  98      // If require session is set then we need to make sure we're legit
  99      if (AmpConfig::get('require_session')) {
 100          if (!AmpConfig::get('require_localnet_session') AND Access::check_network('network',$GLOBALS['user']->id,'5')) {
 101              debug_event('play', 'Streaming access allowed for local network IP ' . $_SERVER['REMOTE_ADDR'],'5');
 102          } else if (!Session::exists('stream', $sid)) {
 103              // No valid session id given, try with cookie session from web interface
 104              $sid = $_COOKIE[AmpConfig::get('session_name')];
 105              if (!Session::exists('interface', $sid)) {
 106                  debug_event('UI::access_denied', 'Streaming access denied: ' . $GLOBALS['user']->username . "'s session has expired", 3);
 107                  header('HTTP/1.1 403 Session Expired');
 108                  exit;
 109              }
 110          }
 111  
 112          // Now that we've confirmed the session is valid
 113          // extend it
 114          Session::extend($sid, 'stream');
 115      }
 116  
 117      /* Update the users last seen information */
 118      $GLOBALS['user']->update_last_seen();
 119  } else {
 120      $secret = $_REQUEST['share_secret'];
 121      $share = new Share($share_id);
 122  
 123      if (!$share->is_valid($secret, 'stream')) {
 124          header('HTTP/1.1 403 Access Unauthorized');
 125          exit;
 126      }
 127  
 128      if ($type != 'song' || !$share->is_shared_song($oid)) {
 129          header('HTTP/1.1 403 Access Unauthorized');
 130          exit;
 131      }
 132  
 133      $GLOBALS['user'] = new User($share->user);
 134      Preference::init();
 135  }
 136  
 137  /* If we are in demo mode.. die here */
 138  if (AmpConfig::get('demo_mode') || (!Access::check('interface','25') )) {
 139      debug_event('UI::access_denied', "Streaming Access Denied:" .AmpConfig::get('demo_mode') . "is the value of demo_mode. Current user level is " . $GLOBALS['user']->access,'3');
 140      UI::access_denied();
 141      exit;
 142  }
 143  
 144  /*
 145     If they are using access lists let's make sure
 146     that they have enough access to play this mojo
 147  */
 148  if (AmpConfig::get('access_control')) {
 149      if (!Access::check_network('stream',$GLOBALS['user']->id,'25') AND
 150          !Access::check_network('network',$GLOBALS['user']->id,'25')) {
 151          debug_event('UI::access_denied', "Streaming Access Denied: " . $_SERVER['REMOTE_ADDR'] . " does not have stream level access",'3');
 152          UI::access_denied();
 153          exit;
 154      }
 155  } // access_control is enabled
 156  
 157  // Handle playlist downloads
 158  if ($type == 'playlist' && isset($playlist_type)) {
 159      $playlist = new Stream_Playlist($oid);
 160      // Some rudimentary security
 161      if ($uid != $playlist->user) {
 162          UI::access_denied();
 163          exit;
 164      }
 165      $playlist->generate_playlist($playlist_type, false);
 166      exit;
 167  }
 168  
 169  /**
 170   * If we've got a tmp playlist then get the
 171   * current song, and do any other crazyness
 172   * we need to
 173   */
 174  if ($demo_id) {
 175      $democratic = new Democratic($demo_id);
 176      $democratic->set_parent();
 177  
 178      // If there is a cooldown we need to make sure this song isn't a repeat
 179      if (!$democratic->cooldown) {
 180          /* This takes into account votes etc and removes the */
 181          $oid = $democratic->get_next_object();
 182      } else {
 183          // Pull history
 184          $song_cool_check = 0;
 185          $oid = $democratic->get_next_object($song_cool_check);
 186          $oids = $democratic->get_cool_songs();
 187          while (in_array($oid,$oids)) {
 188              $song_cool_check++;
 189              $oid = $democratic->get_next_object($song_cool_check);
 190              if ($song_cool_check >= '5') { break; }
 191          } // while we've got the 'new' song in old the array
 192  
 193      } // end if we've got a cooldown
 194  } // if democratic ID passed
 195  
 196  /**
 197   * if we are doing random let's pull the random object
 198   */
 199  if ($random) {
 200      if ($_REQUEST['start'] < 1) {
 201          $oid = Random::get_single_song($_REQUEST['random_type']);
 202          // Save this one in case we do a seek
 203          $_SESSION['random']['last'] = $oid;
 204      } else {
 205          $oid = $_SESSION['random']['last'];
 206      }
 207  } // if random
 208  
 209  if ($type == 'song') {
 210      /* Base Checks passed create the song object */
 211      $media = new Song($oid);
 212      $media->format();
 213  } else if ($type == 'song_preview') {
 214      $media = new Song_Preview($oid);
 215      $media->format();
 216  } else {
 217      $media = new Video($oid);
 218      $media->format();
 219  }
 220  
 221  if ($media->catalog) {
 222      // Build up the catalog for our current object
 223      $catalog = Catalog::create_from_id($media->catalog);
 224  
 225      /* If the song is disabled */
 226      if (!make_bool($media->enabled)) {
 227          debug_event('Play', "Error: $media->file is currently disabled, song skipped", '5');
 228          // Check to see if this is a democratic playlist, if so remove it completely
 229          if ($demo_id && isset($democratic)) { $democratic->delete_from_oid($oid, 'song'); }
 230          header('HTTP/1.1 404 File Disabled');
 231          exit;
 232      }
 233  
 234      // If we are running in Legalize mode, don't play songs already playing
 235      if (AmpConfig::get('lock_songs')) {
 236          if (!Stream::check_lock_media($media->id,get_class($media))) {
 237              exit;
 238          }
 239      }
 240  
 241      $media = $catalog->prepare_media($media);
 242  } else {
 243      // No catalog, must be song preview or something like that => just redirect to file
 244      header('Location: ' . $media->file);
 245      $media = null;
 246  }
 247  if ($media == null) {
 248      // Handle democratic removal
 249      if ($demo_id && isset($democratic)) {
 250          $democratic->delete_from_oid($oid, 'song');
 251      }
 252      exit;
 253  }
 254  
 255  /* If we don't have a file, or the file is not readable */
 256  if (!$media->file || !Core::is_readable(Core::conv_lc_file($media->file))) {
 257  
 258      // We need to make sure this isn't democratic play, if it is then remove
 259      // the song from the vote list
 260      if (is_object($tmp_playlist)) {
 261          $tmp_playlist->delete_track($oid);
 262      }
 263      // FIXME: why are these separate?
 264      // Remove the song votes if this is a democratic song
 265      if ($demo_id && isset($democratic)) { $democratic->delete_from_oid($oid, 'song'); }
 266  
 267      debug_event('play', "Song $media->file ($media->title) does not have a valid filename specified", 2);
 268      header('HTTP/1.1 404 Invalid song, file not found or file unreadable');
 269      exit;
 270  }
 271  
 272  // don't abort the script if user skips this song because we need to update now_playing
 273  ignore_user_abort(true);
 274  
 275  // Format the song name
 276  $media_name = $media->f_artist_full . " - " . $media->title . "." . $media->type;
 277  
 278  header('Access-Control-Allow-Origin: *');
 279  
 280  // Generate browser class for sending headers
 281  $browser = new Horde_Browser();
 282  
 283  /* If they are just trying to download make sure they have rights
 284   * and then present them with the download file
 285   */
 286  if ($_GET['action'] == 'download' AND AmpConfig::get('download')) {
 287  
 288      debug_event('play', 'Downloading file...', 5);
 289      // STUPID IE
 290      $media->format_pattern();
 291      $media_name = str_replace(array('?','/','\\'),"_",$media->f_file);
 292  
 293      $browser->downloadHeaders($media_name,$media->mime,false,$media->size);
 294      $fp = fopen($media->file,'rb');
 295      $bytesStreamed = 0;
 296  
 297      if (!is_resource($fp)) {
 298          debug_event('Play',"Error: Unable to open $media->file for downloading",'2');
 299          exit();
 300      }
 301  
 302      // Check to see if we should be throttling because we can get away with it
 303      if (AmpConfig::get('rate_limit') > 0) {
 304          while (!feof($fp)) {
 305              echo fread($fp,round(AmpConfig::get('rate_limit')*1024));
 306              $bytesStreamed += round(AmpConfig::get('rate_limit')*1024);
 307              flush();
 308              sleep(1);
 309          }
 310      } else {
 311          fpassthru($fp);
 312      }
 313  
 314      fclose($fp);
 315      exit();
 316  
 317  } // if they are trying to download and they can
 318  
 319  // Prevent the script from timing out
 320  set_time_limit(0);
 321  
 322  // We're about to start. Record this user's IP.
 323  if (AmpConfig::get('track_user_ip')) {
 324      $GLOBALS['user']->insert_ip_history();
 325  }
 326  
 327  $force_downsample = false;
 328  if (AmpConfig::get('downsample_remote')) {
 329      if (!Access::check_network('network', $GLOBALS['user']->id, '0')) {
 330          debug_event('play', 'Downsampling enabled for non-local address ' . $_SERVER['REMOTE_ADDR'], 5);
 331          $force_downsample = true;
 332      }
 333  }
 334  
 335  debug_event('play', 'Playing file ('.$media->file.'}...', 5);
 336  debug_event('play', 'Media type {'.$media->type.'}', 5);
 337  
 338  $cpaction = $_REQUEST['custom_play_action'];
 339  // Determine whether to transcode
 340  $transcode = false;
 341  // transcode_to should only have an effect if the song is the wrong format
 342  $transcode_to = $transcode_to == $media->type ? null : $transcode_to;
 343  
 344  debug_event('play', 'Custom play action {'.$cpaction.'}', 5);
 345  debug_event('play', 'Transcode to {'.$transcode_to.'}', 5);
 346  
 347  // If custom play action, do not try to transcode
 348  if (!$cpaction) {
 349      $transcode_cfg = AmpConfig::get('transcode');
 350      $valid_types = $media->get_stream_types();
 351      if ($transcode_cfg != 'never' && in_array('transcode', $valid_types)) {
 352          if ($transcode_to) {
 353              $transcode = true;
 354              debug_event('play', 'Transcoding due to explicit request for ' . $transcode_to, 5);
 355          } else if ($transcode_cfg == 'always') {
 356              $transcode = true;
 357              debug_event('play', 'Transcoding due to always', 5);
 358          } else if ($force_downsample) {
 359              $transcode = true;
 360              debug_event('play', 'Transcoding due to downsample_remote', 5);
 361          } else if (!in_array('native', $valid_types)) {
 362              $transcode = true;
 363              debug_event('play', 'Transcoding because native streaming is unavailable', 5);
 364          } else {
 365              debug_event('play', 'Decided not to transcode', 5);
 366          }
 367      } else if ($transcode_cfg != 'never') {
 368          debug_event('play', 'Transcoding is not enabled for this media type. Valid types: {'.json_encode($valid_types).'}', 5);
 369      } else {
 370          debug_event('play', 'Transcode disabled in user settings.', 5);
 371      }
 372  }
 373  
 374  if ($transcode) {
 375      $transcoder = Stream::start_transcode($media, $transcode_to, $bitrate);
 376      $fp = $transcoder['handle'];
 377      $media_name = $media->f_artist_full . " - " . $media->title . "." . $transcoder['format'];
 378  } else if ($cpaction) {
 379      $transcoder = $media->run_custom_play_action($cpaction, $transcode_to);
 380      $fp = $transcoder['handle'];
 381      $transcode = true;
 382  } else {
 383      $fp = fopen(Core::conv_lc_file($media->file), 'rb');
 384  }
 385  
 386  if ($transcode) {
 387      // Content-length guessing if required by the player.
 388      // Otherwise it shouldn't be used as we are not really sure about final length when transcoding
 389      // Should also support video, but video implementation as to be reviewed first!
 390      if (get_class($media) == 'Song' && $_REQUEST['content_length'] == 'required') {
 391          $max_bitrate = Stream::get_allowed_bitrate($media);
 392          if ($media->time > 0 && $max_bitrate > 0) {
 393              $stream_size = ($media->time * $max_bitrate * 1000) / 8;
 394          } else {
 395              debug_event('play', 'Bad media duration / Max bitrate. Content-length calculation skipped.', 5);
 396              $stream_size = null;
 397          }
 398      } else {
 399          $stream_size = null;
 400      }
 401  } else {
 402      $stream_size = $media->size;
 403  }
 404  
 405  if (!is_resource($fp)) {
 406      debug_event('play', "Failed to open $media->file for streaming", 2);
 407      exit();
 408  }
 409  
 410  header('ETag: ' . $media->id);
 411  // Put this song in the now_playing table only if it's a song for now...
 412  if (get_class($media) == 'Song') {
 413      Stream::insert_now_playing($media->id, $uid, $media->time, $sid, get_class($media));
 414  }
 415  
 416  // Handle Content-Range
 417  
 418  $start = 0;
 419  $end = 0;
 420  sscanf($_SERVER['HTTP_RANGE'], "bytes=%d-%d", $start, $end);
 421  
 422  if ($start > 0 || $end > 0) {
 423      // Calculate stream size from byte range
 424      if (isset($end)) {
 425          $end = min($end, $media->size - 1);
 426          $stream_size = ($end - $start) + 1;
 427      } else {
 428          $stream_size = $media->size - $start;
 429      }
 430  
 431      if ($stream_size == null) {
 432          debug_event('play', 'Content-Range header received, which we cannot fulfill due to unknown final length (transcoding?)', 2);
 433      } else {
 434          if ($transcode) {
 435              debug_event('play', 'We should transcode only for a calculated frame range, but not yet supported here.', 2);
 436                  $stream_size = null;
 437          } else {
 438              debug_event('play', 'Content-Range header received, skipping ' . $start . ' bytes out of ' . $media->size, 5);
 439              fseek($fp, $start);
 440  
 441              $range = $start . '-' . $end . '/' . $media->size;
 442              header('HTTP/1.1 206 Partial Content');
 443              header('Content-Range: bytes ' . $range);
 444          }
 445      }
 446  } else {
 447      debug_event('play','Starting stream of ' . $media->file . ' with size ' . $media->size, 5);
 448  }
 449  
 450  if ($transcode) {
 451      header('Accept-Ranges: none');
 452  } else {
 453      header('Accept-Ranges: bytes');
 454  }
 455  
 456  $mime = ($transcode && isset($transcoder))
 457      ? $media->type_to_mime($transcoder['format'])
 458      : $media->mime;
 459  
 460  $browser->downloadHeaders($media_name, $mime, false, $stream_size);
 461  
 462  $bytes_streamed = 0;
 463  
 464  // Actually do the streaming
 465  do {
 466      $read_size = $transcode
 467          ? 2048
 468          : min(2048, $stream_size - $bytes_streamed);
 469      $buf = fread($fp, $read_size);
 470      print($buf);
 471      ob_flush();
 472      $bytes_streamed += strlen($buf);
 473  } while (!feof($fp) && (connection_status() == 0) && ($transcode || $bytes_streamed < $stream_size));
 474  
 475  $real_bytes_streamed = $bytes_streamed;
 476  // Need to make sure enough bytes were sent.
 477  if ($bytes_streamed < $stream_size && (connection_status() == 0)) {
 478      print(str_repeat(' ', $stream_size - $bytes_streamed));
 479      $bytes_streamed = $stream_size;
 480  }
 481  
 482  if ($start > 0) {
 483      debug_event('play', 'Content-Range doesn\'t start from 0, stats should already be registered previously; not collecting stats', 5);
 484  } else if ($real_bytes_streamed > 0) {
 485      // FIXME: support other media types
 486      if (get_class($media) == 'Song' && empty($share_id)) {
 487          if ($_SERVER['REQUEST_METHOD'] != 'HEAD') {
 488              debug_event('play', 'Registering stats for {'.$media->title.'}...', '5');
 489              $sessionkey = Stream::$session;
 490              //debug_event('play', 'Current session key {'.$sessionkey.'}', '5');
 491              $agent = Session::agent($sessionkey);
 492              //debug_event('play', 'Current session agent {'.$agent.'}', '5');
 493              $GLOBALS['user']->update_stats($media->id, $agent);
 494              $media->set_played();
 495          }
 496      }
 497  }
 498  
 499  // If this is a democratic playlist remove the entry.
 500  // We do this regardless of play amount.
 501  if ($demo_id && isset($democratic)) { $democratic->delete_from_oid($oid,'song'); }
 502  
 503  if ($transcode && isset($transcoder)) {
 504      if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
 505          fread($transcoder['stderr'], 8192);
 506          fclose($transcoder['stderr']);
 507      }
 508      fclose($fp);
 509      proc_close($transcoder['process']);
 510      debug_event('transcode_cmd', $stderr, 5);
 511  } else {
 512      fclose($fp);
 513  }
 514  
 515  debug_event('play', 'Stream ended at ' . $bytes_streamed . ' (' . $real_bytes_streamed . ') bytes out of ' . $stream_size, 5);

title

Description

title

Description

title

Description

title

title

Body