. */ require('components.php'); // this script requires a long time when generating a lot of content ini_set('max_execution_time', max(300,ini_get('max_execution_time'))); // required for some functions date_default_timezone_set('Etc/UTC'); function openbroadcaster_show_times_sort($a,$b) { if($a['start']==$b['start']) return 0; return ($a['start'] < $b['start']) ? -1 : 1; } // TODO: refactoring needed. this is a bit mind boggling (especially schedule and default playlist stuff). class Remote { private $io; private $load; private $user; private $db; private $devmode; private $player; private $buffer; private $localtime; private $xml; private $TimeslotsModel; private $SchedulesModel; private $MediaModel; private $PlayersModel; public function __construct() { $this->io = OBFIO::get_instance(); $this->load = OBFLoad::get_instance(); $this->user = OBFUser::get_instance(); $this->db = OBFDB::get_instance(); $this->TimeslotsModel = $this->load->model('Timeslots'); $this->SchedulesModel = $this->load->model('Schedules'); $this->MediaModel = $this->load->model('Media'); $this->PlayersModel = $this->load->model('Players'); $this->PlaylistsModel = $this->load->model('Playlists'); // development/testing mode if(!empty($_GET['devmode']) && defined('OB_REMOTE_DEBUG') && $_GET['devmode']==OB_REMOTE_DEBUG) { $_POST=$_GET; $this->devmode=TRUE; } else $this->devmode=FALSE; // get our action if(!empty($_GET['action'])) $action = $_GET['action']; else $action = null; // authenticate the player, load player information. if(empty($_POST['id'])) $this->send_xml_error('player id required'); $this->db->where('id',$_POST['id']); $this->player = $this->db->get_one('players'); if($this->player['parent_player_id']) { $this->db->where('id',$this->player['parent_player_id']); $this->parent_player = $this->db->get_one('players'); } if($this->player['parent_player_id'] && $this->player['use_parent_playlist']) { $this->default_playlist_id = $this->parent_player['default_playlist_id']; $this->default_playlist_player_id = $this->player['parent_player_id']; } else { $this->default_playlist_id = $this->player['default_playlist_id']; $this->default_playlist_player_id = $this->player['id']; } if($this->player['parent_player_id'] && $this->player['use_parent_schedule']) { $this->schedule_player_id = $this->player['parent_player_id']; } else { $this->schedule_player_id = $this->player['id']; } if($this->player['parent_player_id'] && $this->player['use_parent_schedule'] && $this->player['use_parent_dynamic']) { $this->cache_player_id = $this->player['parent_player_id']; } else { $this->cache_player_id = $this->player['id']; } // see if password matches (using old/bad hashing or new/good hashing). if(!empty($this->player['password'])) { $password_info = password_get_info($this->player['password']); if($password_info['algo']==0) $password_match = $this->player['password']==sha1(OB_HASH_SALT.$_POST['pw']); else $password_match = password_verify($_POST['pw'].OB_HASH_SALT, $this->player['password']); } else $password_match = false; // if password is correct but needs rehashing, do that now and store in db. if($password_match && password_needs_rehash($this->player['password'], PASSWORD_DEFAULT)) { $new_password_hash = password_hash($_POST['pw'].OB_HASH_SALT, PASSWORD_DEFAULT); $this->db->where('id',$this->player['id']); $this->db->update('players',array('password'=>$new_password_hash)); } if(!$this->devmode && (!$this->player || !$password_match || ($_SERVER['REMOTE_ADDR']!=$this->player['ip_address'] && $this->player['ip_address']!='') )) $this->send_xml_error('invalid id/password/ip combination'); if($action=='schedule' || $action=='emerg') { if(isset($_POST['buffer'])) $this->buffer = $_POST['buffer']*86400; elseif(isset($_POST['hbuffer'])) $this->buffer = $_POST['hbuffer']*3600; } if(($action=='schedule' || $action=='emerg') && empty($this->buffer)) $this->send_xml_error('required information missing'); $this->localtime=time(); // update our 'last connect' date $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect'=>$this->localtime,'last_ip_address'=>$_SERVER['REMOTE_ADDR'])); // initialize xml stuff. $this->xml=new SimpleXMLElement(''); if($action=='version') { if(!empty($_POST['version'])) { $this->PlayersModel('update_version',$this->player['id'],$_POST['version']); //add function to update location if(!empty($_POST['longitude']) || !empty($_POST['latitude'])) { $this->PlayersModel('update_location',$this->player['id'],$_POST['longitude'],$_POST['latitude']); } } if(is_file('VERSION')) { $version = trim(file_get_contents('VERSION')); header ("content-type: application/json"); echo json_encode($version); } } elseif($action=='schedule') { $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect_schedule'=>$this->localtime)); $this->schedule(); // reset/untoggle any last connect warning $this->db->where('event','player_last_connect_schedule_warning'); $this->db->where('player_id',$this->player['id']); $this->db->update('notices',array('toggled'=>0)); } elseif($action=='emerg') { $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect_emergency'=>$this->localtime)); $this->emergency(); // reset/untoggle any last connect warning $this->db->where('event','player_last_connect_emergency_warning'); $this->db->where('player_id',$this->player['id']); $this->db->update('notices',array('toggled'=>0)); } elseif($action=='playlog_status') { $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect_playlog'=>$this->localtime)); $this->playlog_status(); // reset/untoggle any last connect warning $this->db->where('event','player_last_connect_playlog_warning'); $this->db->where('player_id',$this->player['id']); $this->db->update('notices',array('toggled'=>0)); } elseif($action=='playlog_post') { $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect_playlog'=>$this->localtime)); $this->playlog_post(); // reset/untoggle any last connect warning $this->db->where('event','player_last_connect_playlog_warning'); $this->db->where('player_id',$this->player['id']); $this->db->update('notices',array('toggled'=>0)); } elseif($action=='media') { $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect_media'=>$this->localtime)); $this->media(); } elseif($action=='thumbnail') { $this->db->where('id',$this->player['id']); $this->db->update('players',array('last_connect_media'=>$this->localtime)); $this->thumbnail(); } elseif($action=='now_playing') $this->update_now_playing(); } // shortcut to use $this->ModelName('method',arg1,arg2,...). public function __call($name,$args) { if(!isset($this->$name)) { $stack = debug_backtrace(); trigger_error('Call to undefined method '.$name.' ('.$stack[0]['file'].':'.$stack[0]['line'].')', E_USER_ERROR); } $obj = $this->$name; return call_user_func_array($obj,$args); } private function schedule() { // a little buffer... $localtime=strtotime("-1 minute",$this->localtime); // build the schedule XML $schedxml=$this->xml->addChild('schedule'); $end_timestamp=$localtime + $this->buffer + 60; $shows = $this->SchedulesModel('get_shows',$localtime,$end_timestamp,$this->schedule_player_id); $show_times = array(); foreach($shows as $show) { // create start datetime object (used for playlist resolve) $show_start = new DateTime('@'.$show['start'], new DateTimeZone('UTC')); $show_start->setTimezone(new DateTimeZone($this->player['timezone'])); // skip this show if linein but not supported (will get default playlist instead later if available) if($show['item_type']=='linein' && empty($this->player['support_linein'])) continue; $media_items = false; $showxml = $schedxml->addChild('show'); $showxml->addChild('id',$show['id']); $showxml->addChild('date',gmdate('Y-m-d',$show['start'])); $showxml->addChild('time',gmdate('H:i:s',$show['start'])); $showxml->addChild('type',$show['type']); // determine show name (timeslot name) for this playlist. // TODO this only considers the start of the timeslot... what if playlist overlaps timeslots? // best option might be to not allow playlist to overlap timeslots (which might be useful in itself). $timeslot = $this->TimeslotsModel('get_permissions',$show['start'],$show['start']+1,$this->schedule_player_id); // $timeslot = $timeslot[2]; if(!empty($timeslot)) $showxml->addChild('name',$timeslot[0]['description']); $mediaxml = $showxml->addChild('media'); if($show['item_type']=='linein') { $media_items = array(array('type'=>'linein','duration'=>$show['duration'])); } elseif($show['item_type']=='media') { $this->db->where('id',$show['item_id']); $media = $this->db->get_one('media'); if($media) { if(empty($timeslot)) $showxml->addChild('name',''); $showxml->addChild('description',$media['artist'].' - '.$media['title']); $showxml->addChild('last_updated',$media['updated']); $media_items=array($media); } } elseif($show['item_type']=='playlist') { $this->db->where('id',$show['item_id']); $playlist = $this->db->get_one('playlists'); $showxml->addChild('description',$playlist['description']); // if we didn't get our show name from the timeslot, then use the playlist name as the show name. if(empty($timeslot)) $showxml->addChild('name',$playlist['name']); // see if we have selected media in our cache. /* $this->db->where('schedule_id',$show['id']); if(!empty($show['recurring_start'])) { $this->db->where('mode','recurring'); $this->db->where('start',$show['start']); } else $this->db->where('mode','once'); $this->db->where('player_id',$this->player['id']); */ $this->db->where('show_expanded_id',$show['exp_id']); $this->db->where('start',$show['start']); $this->db->where('player_id',$this->player['id']); $cache = $this->db->get_one('shows_cache'); if($cache) { $media_items = json_decode($cache['data']); foreach($media_items as $index=>$tmp) $media_items[$index]=get_object_vars($tmp); // convert object to assoc. array $showxml->addChild('last_updated',$cache['created']); } // are we using a parent player for cache? elseif($this->cache_player_id != $this->player['id']) // this was set to $this->player['player_id'] which i'm quite sure was wrong... (fyi in case i broke something). { /* $this->db->where('schedule_id',$show['id']); if(!empty($show['recurring_start'])) { $this->db->where('mode','recurring'); $this->db->where('start',$show['start']); } else $this->db->where('mode','once'); $this->db->where('player_id',$this->cache_player_id); */ $this->db->where('show_expanded_id',$show['exp_id']); $this->db->where('start',$show['start']); $this->db->where('player_id',$this->cache_player_id); $cache = $this->db->get_one('shows_cache'); // we are supposed to use a parent player for cache, but that player doesn't have the cached item yet. if(!$cache) { $media_items = $this->PlaylistsModel('resolve', $playlist['id'], $this->schedule_player_id, $player['parent_player_id'], $show_start, $show['duration']); $cache_created = time(); $this->db->insert('shows_cache',[ 'player_id'=>$this->cache_player_id, 'show_expanded_id'=>$show['exp_id'], 'start'=>$show['start'], 'duration'=>$show['duration'], 'data'=>json_encode($media_items), 'created'=>$cache_created ]); } // oh, we do have cache from parent... let's get media items from it. else { $media_items = json_decode($cache['data']); foreach($media_items as $index=>$tmp) $media_items[$index]=get_object_vars($tmp); // convert object to assoc. array } // now we should really have parent player cache ... copy to our main (child) player. $media_items = $this->convert_station_ids($media_items); $cache_created = time(); $showxml->addChild('last_updated',$cache_created); $this->db->insert('shows_cache',[ 'player_id'=>$this->player['id'], 'show_expanded_id'=>$show['exp_id'], 'start'=>$show['start'], 'duration'=>$show['duration'], 'data'=>json_encode($media_items), 'created'=>$cache_created ]); } // do we still not have media items for some reason? no cache, no parent cache, or something went wrong... if($media_items===false) { $media_items = $this->PlaylistsModel('resolve', $playlist['id'], $this->schedule_player_id, false, $show_start, $show['duration']); $cache_created = time(); $showxml->addChild('last_updated',$cache_created); $this->db->insert('shows_cache',[ 'player_id'=>$this->player['id'], 'show_expanded_id'=>$show['exp_id'], 'start'=>$show['start'], 'duration'=>$show['duration'], 'data'=>json_encode($media_items), 'created'=>$cache_created ]); } } $order_count = 0; $media_offset = 0.0; $media_audio_offset = 0.0; $media_image_offset = 0.0; foreach($media_items as $media_item) { if($show['type']=='standard' && $media_offset > $show['duration']) break; if($show['type']=='advanced') { if(max($media_audio_offset,$media_image_offset) > $show['duration']) break; if($media_item['type']=='audio') { $media_offset = $media_audio_offset; $media_audio_offset += $media_item['duration'] - ($media_item['crossfade'] ?? 0); } elseif($media_item['type']=='image') { $media_offset = $media_image_offset; $media_image_offset += $media_item['duration']; } else { $media_offset = max($media_audio_offset, $media_image_offset); $media_audio_offset = $media_offset + $media_item['duration']; $media_image_offset = $media_offset + $media_item['duration']; } } $itemxml=$mediaxml->addChild('item'); $this->media_item_xml($itemxml,$media_item,$order_count,$media_offset); if($show['type']=='standard' || $show['type']=='live_assist') $media_offset += ($media_item['duration'] ?? 0) - ($media_item['crossfade'] ?? 0); $order_count++; } // live assist shows always use specified show duration (total media duration means nothing because of breakpoints) if($show['type']=='live_assist') $show_actual_duration = $show['duration']; else $show_actual_duration = $media_offset; // if the show-specified duration is less than the actual duration (total of media durations), then use the the show-specified so the next show isn't cut of at the beginning. // otherwise, use the actual duration (shorter) so that we can fill in the rest with 'default playlist' material. $showxml->addChild('duration',$show['duration'] < $show_actual_duration ? $show['duration'] : $show_actual_duration); $show_times[]=array('start'=>$show['start'],'end'=>$show['start'] + min($show['duration'],$show_actual_duration)); if($show['item_type']=='playlist' && $show['type']=='live_assist') $this->add_liveassist_buttons($playlist['id'], $show, $showxml); // make sure we have media items and if not remove the show if($show['type']!='live_assist' && empty($showxml->media)) { unset($schedxml->show[count($schedxml->show)-1]); } } usort($show_times,"openbroadcaster_show_times_sort"); // fill in blank spots with default playlist, if we have one. if(!empty($this->default_playlist_id)) { // default starting time is now. but we'll check to see whether there is an earlier starting time from a cached default playlist which is still playing. $timestamp_pointer = time(); $this->db->query('SELECT start FROM shows_cache WHERE show_expanded_id IS NULL AND start < '.$this->db->escape($timestamp_pointer).' AND start+duration > '.$this->db->escape($timestamp_pointer)); if($this->db->num_rows()>0) { $cached_default_playlist = $this->db->assoc_row(); $timestamp_pointer = $cached_default_playlist['start']; } $default_playlist_finished = false; for($default_playlist_counter=0;$timestamp_pointer < $end_timestamp;$default_playlist_counter++) { if($default_playlist_counter==0 && (count($show_times)==0 || $show_times[0]['start']>$timestamp_pointer)) { $default_start = $timestamp_pointer; if(count($show_times)==0) $default_end = $end_timestamp; else $default_end = $show_times[0]['start']; $default_start_tmp = $default_start; while($default_start_tmp < $default_end) { if($default_start_tmp > $end_timestamp) break(2); // end of buffer, we're done. // get show content. $showxml = $schedxml->addChild('show'); // add default playlist as a show. (this function returns duration, so we add it to our time). $show_duration = $this->default_playlist_show_xml($showxml,$default_start_tmp,($default_end-$default_start_tmp)); if($show_duration<=0) break(2); // no show duration, cancel. $default_start_tmp = $default_start_tmp + $show_duration; } } if(!empty($show_times[$default_playlist_counter])) { $default_start = ceil($show_times[$default_playlist_counter]['end']); // need ceiling since we store start times in whole numbers. if(count($show_times)>($default_playlist_counter+1)) $default_end = $show_times[$default_playlist_counter+1]['start']; else $default_end = $end_timestamp; // this will be false if there is no gap between shows, or at the end where a show goes over our end timestamp. if($default_start<$default_end) { $default_start_tmp = $default_start; while($default_start_tmp < $default_end) { if($default_start_tmp > $end_timestamp) break(2); // end of buffer, we're done. // get show content. $showxml = $schedxml->addChild('show'); // add default playlist as a show. (this function returns duration, so we add it to our time). $show_duration = $this->default_playlist_show_xml($showxml,$default_start_tmp,($default_end-$default_start_tmp)); if($show_duration<=0) break(2); // no show duration, cancel. $default_start_tmp = $default_start_tmp + $show_duration; } } } $timestamp_pointer = $default_end; } } header ("content-type: text/xml"); echo @$this->xml->asXML(); } private function add_liveassist_buttons($playlist_id, $show, $show_xml) { // create start datetime object (used for playlist resolve) $show_start = new DateTime('@'.$show['start'], new DateTimeZone('UTC')); $show_start->setTimezone(new DateTimeZone($this->player['timezone'])); $buttons_xml = $show_xml->addChild('liveassist_buttons'); $this->db->where('playlist_id',$playlist_id); $this->db->orderby('order_id'); $buttons = $this->db->get('playlists_liveassist_buttons'); foreach($buttons as $button) { $this->db->where('id',$button['button_playlist_id']); $playlist = $this->db->get_one('playlists'); if(!$playlist) continue; // playlist not available. $this->db->where('player_id', $this->player['id']); $this->db->where('start', $show['start']); $this->db->where('playlists_liveassist_button_id', $button['id']); $cache = $this->db->get_one('schedules_liveassist_buttons_cache'); if($cache) { $items = (array) json_decode($cache['data']); $cache_created = $cache['created']; } else { $items = $this->PlaylistsModel('resolve', $button['button_playlist_id'], $this->player['id'], false, $show_start); $cache_created = time(); // $showxml->addChild('last_updated',$cache_created); $this->db->insert('schedules_liveassist_buttons_cache',array('player_id'=>$this->player['id'],'start'=>$show['start'],'playlists_liveassist_button_id'=>$button['id'],'data'=>json_encode($items),'created'=>$cache_created)); } $group_xml = $buttons_xml->addChild('group'); $group_xml->addChild('last_updated', $cache_created); $group_xml->addChild('name',$playlist['name']); $media_xml = $group_xml->addChild('media'); foreach($items as $item) { $item = (array) $item; if($item['type']=='breakpoint') continue; $item_xml = $media_xml->addChild('item'); $this->media_item_xml($item_xml, $item); } } } private function convert_station_ids($media_items) { // no need to swap out station IDs, we're already using parent's ids. if($this->player['use_parent_ids']) return $media_items; // swap out station IDs from parent, with station IDs for child. $new_items = array(); foreach($media_items as $index=>$item) { // this item is a station ID. get a station id from our child player instead. if(!empty($item['is_station_id'])) { $this->db->query('SELECT media.* FROM players_station_ids LEFT JOIN media ON players_station_ids.media_id = media.id WHERE player_id="'.$this->db->escape($this->player['id']).'" order by rand() limit 1;'); $rows = $this->db->assoc_list(); if(count($rows)>0) { // if this station id is an image, how long should we display it for? check player settings. if($rows[0]['type']=='image') $rows[0]['duration'] = $this->player['station_id_image_duration']; $rows[0]['is_station_id'] = true; // add to our media items. $new_items[]=$rows[0]; } } else $new_items[] = $item; } return $new_items; } private function media_item_xml(&$itemxml,$track,$ord=false,$offset=false) { // special handling for 'breakpoint' (not really a media item, more of an instruction). if($track['type']=='breakpoint') { if($ord!==false) $itemxml->addChild('order',$ord); if($offset!==false) $itemxml->addChild('offset',$offset); // offset is replacing 'order' to allow multiple media to play at once. $itemxml->addChild('duration',0); $itemxml->addChild('type',$track['type']); return true; } // get full media metadata if($track['type']=='media') { $media = $this->MediaModel('get_by_id', ['id' => $track['id']]); if(!$media) return false; } $itemxml->addChild('duration',$track['duration']); $itemxml->addChild('type',$track['type']=='media' ? $media['type'] : $track['type']); if($ord!==false) $itemxml->addChild('order',$ord); if($offset!==false) $itemxml->addChild('offset',$offset); // offset is replacing 'order' to allow multiple media to play at once. if($track['type']=='media') { if(!empty($media['is_archived'])) $filerootdir=OB_MEDIA_ARCHIVE; elseif(!empty($media['is_approved'])) $filerootdir=OB_MEDIA; else $filerootdir=OB_MEDIA_UPLOADS; $fullfilepath=$filerootdir.'/'.$media['file_location'][0].'/'.$media['file_location'][1].'/'.$media['filename']; // missing media file // TODO should remove entirely // if(!file_exists($fullfilepath)) return false; $filesize=filesize($fullfilepath); $itemxml->addChild('id',$track['id']); $itemxml->addChild('filename',htmlspecialchars($media['filename'])); $itemxml->addChild('title',htmlspecialchars($media['title'])); $itemxml->addChild('artist',htmlspecialchars($media['artist'])); $itemxml->addChild('hash',$media['file_hash']); $itemxml->addChild('filesize',$filesize); $itemxml->addChild('location',$media['file_location']); $itemxml->addChild('archived',$media['is_archived']); $itemxml->addChild('approved',$media['is_approved']); $itemxml->addChild('thumbnail',$media['thumbnail']); $itemxml->addChild('context',$track['context']); if($track['crossfade'] ?? null) $itemxml->addChild('crossfade',$track['crossfade']); } return true; } // add default playlist to show xml. // max duration considered when adding media items to xml, but not when generating for cache purposes. // this is because max_duration might 'extend' in the future (as more buffer requested by remote). // returns duration. private function default_playlist_show_xml(&$showxml,$start,$max_duration) { if(empty($this->default_playlist_id)) return 0; // create start datetime object (used for playlist resolve) $show_start = new DateTime('@'.$start, new DateTimeZone('UTC')); $show_start->setTimezone(new DateTimeZone($this->player['timezone'])); // get our playlist name to report as the show name (below) $this->db->where('id',$this->default_playlist_id); $playlist = $this->db->get_one('playlists'); if($playlist['type']=='live_assist') $playlist['type']='standard'; // live_assist converted to standard for default playlist. $show_media_items = array(); // see if we have selected media in our cache. $this->db->where('player_id',$this->player['id']); $this->db->where('show_expanded_id',NULL); $this->db->where('start',$start); // $this->db->where('duration',$end-$start); $cache = $this->db->get_one('shows_cache'); if($cache) { $show_media_items = json_decode($cache['data']); foreach($show_media_items as $index=>$tmp) $show_media_items[$index]=get_object_vars($tmp); // convert object to assoc. array $showxml->addChild('last_updated',$cache['created']); $duration = $cache['duration']; } // are we using a parent player for cache (and playlist)? elseif($this->cache_player_id!=$this->player['id'] && $this->player['use_parent_playlist']) { // see if parent has a cache entry. $this->db->where('player_id',$this->cache_player_id); $this->db->where('show_expanded_id',NULL); $this->db->where('start',$start); // $this->db->where('duration',$end-$start); $cache = $this->db->get_one('shows_cache'); // we are supposed to use a parent player for cache, but that player doesn't have the cached item yet. if(!$cache) { $show_media_items = $this->PlaylistsModel('resolve', $this->default_playlist_id, $this->default_playlist_player_id, false, $show_start, $max_duration); $cache_created = time(); $duration = $this->total_items_duration($show_media_items,$playlist['type']=='advanced'); $this->db->insert('shows_cache',[ 'show_expanded_id'=>null, 'player_id'=>$this->cache_player_id, 'start'=>$start, 'duration'=>$duration, 'data'=>json_encode($show_media_items), 'created'=>$cache_created ]); } // oh, we do have cache from parent... let's get media items from it. else { $show_media_items = json_decode($cache['data']); foreach($show_media_items as $index=>$tmp) $show_media_items[$index]=get_object_vars($tmp); // convert object to assoc. array } // now we should really have parent player cache ... copy to our main (child) player. $show_media_items = $this->convert_station_ids($show_media_items); $duration = $this->total_items_duration($show_media_items,$playlist['type']=='advanced'); $cache_created = time(); $showxml->addChild('last_updated',$cache_created); $this->db->insert('shows_cache',[ 'show_expanded_id'=>null, 'player_id'=>$this->player['id'], 'start'=>$start, 'duration'=>$duration, 'data'=>json_encode($show_media_items), 'created'=>$cache_created ]); } // still don't have media items? if(empty($show_media_items)) { $show_media_items = $this->PlaylistsModel('resolve', $this->default_playlist_id, $this->default_playlist_player_id, false, $show_start, $max_duration); $duration = $this->total_items_duration($show_media_items,$playlist['type']=='advanced'); $cache_created = time(); $showxml->addChild('last_updated',$cache_created); $this->db->insert('shows_cache',[ 'show_expanded_id'=>null, 'player_id'=>$this->player['id'], 'start'=>$start, 'duration'=>$duration, 'data'=>json_encode($show_media_items), 'created'=>$cache_created ]); } // generate XML for show/media items. $showxml->addChild('id',0); $showxml->addChild('date',gmdate('Y-m-d',$start)); $showxml->addChild('time',gmdate('H:i:s',$start)); $showxml->addChild('name',$playlist['name']); $showxml->addChild('type',$playlist['type']); $showxml->addChild('description','Default Playlist'); // $showxml->addChild('last_updated',time()); $showxml->addChild('duration',min($max_duration,$duration)); $mediaxml = $showxml->addChild('media'); $order_count = 0; $media_offset = 0.0; $media_audio_offset = 0.0; $media_image_offset = 0.0; foreach($show_media_items as $media_item) { if($media_item['type']=='breakpoint') continue; // completely ignore breakpoints. (live assist converted to standard playlist). if($playlist['type']=='advanced') { if($media_item['type']=='audio') { // if our audio offset is already past the max duration, we don't want to add more audio. if($media_audio_offset >= $max_duration) continue; $media_offset = $media_audio_offset; $media_audio_offset += $media_item['duration'] - ($media_item['crossfade'] ?? 0); } elseif($media_item['type']=='image') { // if our image offset is already past the max duration, we don't want to add more images. if($media_image_offset >= $max_duration) continue; $media_offset = $media_image_offset; $media_image_offset += $media_item['duration']; } else { // if audio or image offset is already past the max duration, we don't want to add anymore anything! // (adding video would start past the max_duration point). if(max($media_audio_offset,$media_image_offset) >= $max_duration) break; $media_offset = max($media_audio_offset, $media_image_offset); $media_audio_offset = $media_offset + $media_item['duration']; $media_image_offset = $media_offset + $media_item['duration']; } } $itemxml=$mediaxml->addChild('item'); $this->media_item_xml($itemxml,$media_item,$order_count,$media_offset); if($playlist['type']=='standard') { $media_offset += $media_item['duration'] - ($media_item['crossfade'] ?? 0);; if($media_offset > $max_duration) break; // our next media offset is beyond max_duration, no more items to add. } $order_count++; } return min($max_duration,$duration); } private function total_items_duration($media_items,$advanced = false) { $media_offset = 0.0; $media_audio_offset = 0.0; $media_image_offset = 0.0; if($advanced) { foreach($media_items as $media_item) { if($media_item['type']=='audio') { $media_audio_offset += $media_item['duration'] - ($media_item['crossfade'] ?? 0); } elseif($media_item['type']=='image') { $media_image_offset += $media_item['duration']; } else { $media_offset = max($media_audio_offset, $media_image_offset); $media_audio_offset = $media_offset + $media_item['duration']; $media_image_offset = $media_offset + $media_item['duration']; } } return ceil(max($media_audio_offset,$media_image_offset)); } else { foreach($media_items as $media_item) $media_offset += $media_item['duration'] - ($media_item['crossfade'] ?? 0); return ceil($media_offset); } } private function emergency() { if($this->player['parent_player_id'] && $this->player['use_parent_emergency']) $broadcasts = $this->get_upcoming_emergency_broadcasts($this->player['parent_player_id'],time()+$this->buffer); else $broadcasts = $this->get_upcoming_emergency_broadcasts($this->player['id'],time()+$this->buffer); $schedxml=$this->xml->addChild('emergency_broadcasts'); if(!empty($broadcasts)) foreach($broadcasts as $broadcast) { $this->db->where('id',$broadcast['item_id']); $mediaInfo=$this->db->get_one('media'); if(empty($broadcast['duration'])) $broadcast_duration=$mediaInfo['duration']; else $broadcast_duration=$broadcast['duration']; if(!empty($mediaInfo['is_archived'])) $filerootdir=OB_MEDIA_ARCHIVE; elseif(!empty($mediaInfo['is_approved'])) $filerootdir=OB_MEDIA; else $filerootdir=OB_MEDIA_UPLOADS; $fullfilepath=$filerootdir.'/'.$mediaInfo['file_location'][0].'/'.$mediaInfo['file_location'][1].'/'.$mediaInfo['filename']; $filesize=filesize($fullfilepath); // set start if we don't have one... (starts immediately). remote wants something here. if(empty($broadcast['start'])) $broadcast['start']='0'; // set end if we don't have one... (plays indefiniteyl). remote wants something here. if(empty($broadcast['stop'])) $broadcast['stop']='2147483647'; $broadcastxml = $schedxml->addChild('broadcast'); $broadcastxml->addChild('id',$broadcast['id']); $broadcastxml->addChild('start_timestamp',$broadcast['start']); $broadcastxml->addChild('end_timestamp',$broadcast['stop']); $broadcastxml->addChild('frequency',$broadcast['frequency']); $broadcastxml->addChild('artist',htmlspecialchars($mediaInfo['artist'])); $broadcastxml->addChild('filename',htmlspecialchars($mediaInfo['filename'])); $broadcastxml->addChild('title',htmlspecialchars($mediaInfo['title'])); $broadcastxml->addChild('media_id',htmlspecialchars($mediaInfo['id'])); $broadcastxml->addChild('duration',htmlspecialchars($broadcast_duration)); $broadcastxml->addChild('media_type',htmlspecialchars($mediaInfo['type'])); $broadcastxml->addChild('hash',$mediaInfo['file_hash']); $broadcastxml->addChild('filesize',$filesize); $broadcastxml->addChild('location',$mediaInfo['file_location']); $broadcastxml->addChild('archived',$mediaInfo['flag_delete']); $broadcastxml->addChild('approved',$mediaInfo['is_approved']); } header ("content-type: text/xml"); echo $this->xml->asXML(); // die(); } private function playlog_status() { $this->db->where('player_id',$this->player['id']); $this->db->orderby('timestamp','desc'); $last_entry = $this->db->get_one('playlog'); if(empty($last_entry)) $last_timestamp=0; else $last_timestamp=$last_entry['timestamp']; $replyxml=$this->xml->addChild('playlog_status'); $replyxml->addChild('last_timestamp',$last_timestamp); header('content-type: text/xml'); echo $this->xml->asXML(); } private function playlog_post() { if(empty($_POST['data'])) { $this->send_xml_error('missing XML post data'); die(); } $data=new SimpleXMLElement($_POST['data']); $playlog=$data->playlog; foreach($playlog->entry as $entry) { $entryArray=(array) $entry; // if a value is an object, that is because it has no value // in this case, it is left as a SimpleXMLElement object (empty) instead of being converted to a string. // so here we convert to a empty string foreach($entryArray as $index=>$value) { if(is_object($value)) $entryArray[$index]=''; } // db inconsistency between web app and remote. $entryArray['timestamp']=$entryArray['datetime']; unset($entryArray['datetime']); $entryArray['player_id']=$this->player['id']; $this->addedit_playlog($entryArray); } $replyxml=$this->xml->addChild('playlog_post'); $replyxml->addChild('status','success'); header('content-type: text/xml'); echo $this->xml->asXML(); } private function media() { if(empty($_POST['media_id'])) die(); $this->db->where('id',$_POST['media_id']); $media = $this->db->get_one('media'); if(empty($media)) die(); if($media['is_archived']==1) $filedir=OB_MEDIA_ARCHIVE; elseif($media['is_approved']==0) $filedir=OB_MEDIA_UPLOADS; else $filedir=OB_MEDIA; $filedir.='/'.$media['file_location'][0].'/'.$media['file_location'][1]; $fullpath=$filedir.'/'.$media['filename']; if(!file_exists($fullpath)) die(); header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header("Content-Transfer-Encoding: binary"); header("Content-Length: ".filesize($fullpath)); readfile($fullpath); } private function thumbnail() { if(empty($_POST['media_id'])) die(); $this->db->where('id',$_POST['media_id']); $media = $this->db->get_one('media'); if(empty($media)) die(); $filedir = '/'.$media['file_location'][0].'/'.$media['file_location'][1]; $fullpath = OB_CACHE.'/thumbnails/'.$filedir.'/'.$media['id'].'.jpg'; if(!file_exists($fullpath)) die(); header('Content-Type: image/jpeg'); header("Content-Length: ".filesize($fullpath)); readfile($fullpath); } private function send_xml_error($message) { $xml=new SimpleXMLElement(''); $xml->addChild('error',$message); header ("content-type: text/xml"); echo $xml->asXML(); die(); } private function get_upcoming_emergency_broadcasts($player,$timelimit) { $now=time(); $addsql=' where player_id='.$player.' and (start<='.$timelimit.' or start IS NULL) and (stop>'.$now.' or stop IS NULL ) '; $sql='select *,TIME_TO_SEC(duration) as duration from emergencies'.$addsql.' order by start'; $this->db->query($sql); $r=$this->db->assoc_list(); return $r; } private function addedit_playlog($datatmp) { $useVals=array('player_id','media_id','artist','title','timestamp','context','emerg_id','notes'); foreach($useVals as $val) $data[$val]=$datatmp[$val]; // convert timestamp to mysql format // $data['datetime']=date('Y-m-d H:i:s',$data['datetime']); if($this->verify_playlog($data)) $this->db->insert('playlog',$data); } private function verify_playlog($data) { foreach($data as $key=>$value) { $$key=$value; $dbcheck[]='`'.$key.'`="'.$this->db->escape($value).'"'; } if(empty($player_id) || !isset($media_id) || empty($timestamp)) $error="Required field is missing."; elseif($context!='show' && $context!='emerg' && $context!='fallback') $error="Context is invalid."; elseif($context=='emerg' && !preg_match('/^[0-9]+$/',$emerg_id)) $error="Emergency broadcast ID is invalid or missing."; elseif(!preg_match('/^[0-9]+$/',$player_id)) $error="Player ID is invalid."; elseif(!preg_match('/^[0-9]+$/',$media_id)) $error="Media ID is invalid."; else { $sql='select id from playlog where '.implode($dbcheck,' and '); $this->db->query($sql); // echo $this->db->error(); if($this->db->num_rows()>0) $error='This log entry already exists.'; } if(empty($error)) return TRUE; return false; } private function update_now_playing() { $current_playlist_id = trim($_POST['playlist_id']); $current_playlist_end = trim($_POST['playlist_end']); $current_media_id = trim($_POST['media_id']); $current_media_end = trim($_POST['media_end']); $current_show_name = trim($_POST['show_name']); if(!preg_match('/^[0-9]+$/',$current_playlist_id) || empty($current_playlist_id)) $current_playlist_id = null; if(!preg_match('/^[0-9]+$/',$current_playlist_end) || empty($current_playlist_end)) $current_playlist_end = null; if(!preg_match('/^[0-9]+$/',$current_media_id) || empty($current_media_id)) $current_media_id = null; if(!preg_match('/^[0-9]+$/',$current_media_end) || empty($current_media_end)) $current_media_end = null; if($current_show_name=='') $current_show_name = null; $db['current_playlist_id'] = $current_playlist_id; $db['current_playlist_end'] = $current_playlist_end; $db['current_media_id'] = $current_media_id; $db['current_media_end'] = $current_media_end; $db['current_show_name'] = $current_show_name; $this->db->where('id',$_POST['id']); $this->db->update('players',$db); } } $remote = new Remote();