import re RSS_FEED_FILE_NAME = '.index.php' RSS_FEED_INFO_EXTENSION = 'info' RSS_FEED_SHOW_INDEX = 'index' RSS_FEED_SHOW_IMAGE = 'image.jpg' RSS_FEED_VERSION = '$SPODCAST_VERSION$ ' VERSION_NOT_FOUND = 0 def get_index_version(filename) -> str: with open(filename, 'rb') as f: for line in f.readlines(): m = re.search(RSS_FEED_VERSION + ' (\d+.\d+.\d+)', str(line)) if m: return int(m[1].replace('.','')) return VERSION_NOT_FOUND def RSS_FEED_CODE(version): return r'''title; $feed_description=$info->description; $feed_link=$info->link; } ?> '; // use php to output the " <?php echo $feed_name; ?> <?php echo $feed_name; ?> \n"; echo " ".$info->title."\n"; echo " ".$info->description."\n"; echo " ".$info->guid."\n"; echo " ".$base_url.$info->filename."\n"; echo " filename."\" length=\"".$info->size."\" type=\"".$info->mimetype."\" />\n"; echo " filename."\" medium=\"".$info->medium."\" duration=\"".$info->duration."\" type=\"".$info->mimetype."\" />\n"; echo " ".$info->date."\n"; echo " ".$info->duration."\n"; echo " \n"; } } } } ?> ''' def RSS_INDEX_CODE(bin_path, config_name, version): return r''' [options...]".PHP_EOL; echo " commands: ".implode("|",CLI_COMMANDS).PHP_EOL; die(); } $settings=read(dirname(__FILE__)."/".SETTINGS_INFO); $feeds=read(dirname(__FILE__)."/".FEEDS_INFO); $command=$argv[1]; if($command == "refresh") { list($retval, $result) = refresh_shows($feeds); if ($retval > 0) { echo "An error occurred during refresh, return value was $retval" . PHP_EOL; } echo implode(PHP_EOL, $result); } die(); } # CGI/API $PROTOCOL = (empty($_SERVER['HTTPS'])) ? "http://" : "https://"; $SPODCAST_URL = $PROTOCOL . $_SERVER['HTTP_HOST'] . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $feeds=get_feeds(dirname(__FILE__)); $settings=get_settings(); $config=get_spodcast_config(); $ERROR_MESSAGE=null; $ERROR_DETAILS=null; $SPODCAST_COMMAND=SPODCAST." -c ".$SPODCAST_CONFIG." --log-level ".$settings['log_level']; function get_feeds($dir) { global $SPODCAST_URL; foreach(glob($dir."/*/".SHOW_INFO) as $show_info) { $episodes=get_episodes(dirname($show_info)); $json=file_get_contents($show_info); $info=json_decode($json); $feeds[$info->link]['title']=$info->title; $feeds[$info->link]['image']=$info->image; $feeds[$info->link]['episodes']=count($episodes); $feeds[$info->link]['last']=date("Y-m-d",strtotime($episodes[0]['date'])); $feeds[$info->link]['directory']=dirname($show_info); $feeds[$info->link]['max']=$info->max ?? 2; $feeds[$info->link]['keep']=$info->keep ?? 5; $feeds[$info->link]['feed']=$SPODCAST_URL.basename(dirname($show_info)); } uasort($feeds, fn ($a, $b) => strnatcmp($a['title'], $b['title'])); store($feeds, FEEDS_INFO); return $feeds; } function get_episodes($dir) { $episodes=[]; foreach(glob($dir."/*.".INFO) as $episode_info) { if(basename($episode_info) == SHOW_INFO) { continue; } $json=file_get_contents($episode_info); $info=json_decode($json); $episodes[]=["filename"=>$info->filename,"date"=>$info->date,"title"=>$info->title]; } usort($episodes, fn ($a, $b) => strtotime($b["date"]) - strtotime($a["date"])); return $episodes; } function get_settings() { global $SPODCAST_URL; $settings=read(SETTINGS_INFO); $settings['spodcast_url']=$settings['spodcast_url'] ?? $SPODCAST_URL; $settings['update_start']=$settings['update_start'] ?? 0; $settings['update_rate']=$settings['update_rate'] ?? 1; $settings['update_enabled']=$settings['update_enabled'] ?? false; $settings['log_level']=$settings['log_level'] ?? LOG_LEVEL; store($settings, SETTINGS_INFO); return $settings; } function get_spodcast_config() { global $SPODCAST_CONFIG; $config=read($SPODCAST_CONFIG); return $config; } function get(&$var, $default=null) { return isset($var) ? $var : $default; } function read($file) { if(is_readable($file)) { $json=file_get_contents($file); $info=json_decode($json, true); } else { $info=[]; } return $info; } function store($info, $file) { $f = fopen($file,'w'); $result = fwrite($f, json_encode($info)); fclose($f); return ($result === false) ? ERROR : SUCCESS; } function cron_signature($crontab, $CRON_SIGNATURE) { $index = 0; foreach ($crontab as $line) { if (strpos($line, $CRON_SIGNATURE) !== false) { return $index; } $index++; } return NOT_FOUND; } function debug($var) { ob_start(); var_dump($var); error_log(ob_get_clean()); } function submit_crontab($crontab) { $retval = null; $output = null; $tempfile=tempnam(sys_get_temp_dir(), 'spodcast'); file_put_contents($tempfile, implode(PHP_EOL,$crontab)); $command="crontab ".$tempfile; exec($command, $output, $retval); unlink($tempfile); return [$retval, $output]; } function get_range($start, $rate) { for ($i=0; $i < $rate; $i++) { $arr[]=(($start%(24/$rate))+($i*(24/$rate)))%24; } return implode(",", $arr); } function background_check_id($id) { $runfile = md5($id).".json"; $log = md5($id).".log"; if (is_readable($runfile)) { $info = read($runfile); if (background_check($info['pid'])) { return true; } unlink($runfile); unlink($log); } return false; } function background_check($pid) { try { $result = shell_exec(sprintf("ps %d", $pid)); if (count(preg_split("/\n/", $result)) > 2) { return true; } } catch(Exception $e) {} return false; } # [ status, output ] return functions function update_scheduler($enable, $start, $rate) { $CRON_SIGNATURE="SPODCAST:".dirname(__FILE__); $crontab=null; $retval=null; exec("crontab -l", $crontab, $retval); if ($retval == 0) { $index=cron_signature($crontab, $CRON_SIGNATURE); if ($enable == true) { if($index !== NOT_FOUND) { array_splice($crontab, $index, 1); } $crontab[]=sprintf("%d %s * * * php %s refresh # %s".PHP_EOL, rand(5,25), get_range($start, $rate), __FILE__, $CRON_SIGNATURE); return submit_crontab($crontab); } else { if ($index !== NOT_FOUND) { array_splice($crontab, $index, 1); $crontab[count($crontab)-1]=rtrim($crontab[count($crontab)-1]).PHP_EOL; return submit_crontab($crontab); } } } else { return [$retval, "failed to update scheduler"]; } } function login($username, $password, $return_output=false) { global $SPODCAST_CONFIG; global $SPODCAST_COMMAND; $output = null; $retval = null; $tempfile=tempnam(sys_get_temp_dir(), 'spodcast'); file_put_contents($tempfile, "$username $password"); $command=$SPODCAST_COMMAND . " -l ".$tempfile." 2>&1"; exec($command, $output, $retval); unlink($tempfile); return [$retval, $output]; } function background_run($command, $id) { $retval=null; $md5 = md5($id); $runfile = $md5.".json"; $output = $md5.".log"; $cmd = sprintf("nohup %s > %s 2>&1 & echo $!", $command, $output); exec($cmd, $pid, $retval); if ($retval == 0 && count($pid) > 0 && $pid[0] > 0) { $info['command']=$command; $info['log']=$output; $info['pid']=(int) $pid[0]; store($info, $runfile); return [$retval, $pid[0]]; } else { return [$retval, 0]; } } function background_add_feed($url, $max) { global $SPODCAST_CONFIG; global $SPODCAST_COMMAND; $output = null; $retval = null; $command=$SPODCAST_COMMAND . " --max-episodes ".(int)$max." ".escapeshellarg($url); list($retval, $pid) = background_run($command, $url); if ($retval == 0 && (int) $pid > 0) { return [$retval, $pid]; } return [$retval, 0]; } function add_feed($url, $max) { global $SPODCAST_CONFIG; global $SPODCAST_COMMAND; $output = null; $retval = null; $command=$SPODCAST_COMMAND . " --max-episodes ".(int)$max." ".escapeshellarg($url)." 2>&1"; exec($command, $output, $retval); return [$retval, $output]; } function update_feed($url, $max, $keep, $feeds) { $output = null; $retval = null; if ($max > 0) { list($retval,$output) = add_feed($url, $max); if ($retval > 0) { return [$retval, $output]; } } $directory=$feeds[$url]['directory']; $episodes=get_episodes($directory); if(count($episodes) > $keep) { $to_delete=array_splice($episodes, $keep); foreach ($to_delete as $episode) { $command="rm -f ".escapeshellarg($directory."/".$episode['filename'])." 2>&1"; exec($command, $output, $retval); if ($retval > 0) { return [$retval, $output]; } $command="rm -f ".escapeshellarg($directory."/".$episode['filename']).".".INFO." 2>&1"; exec($command, $output, $retval); if ($retval > 0) { return [$retval, $output]; } } } return [$retval, $output]; } function delete_feed($url, $return_output=false) { $output = null; $retval = null; $feeds=read(FEEDS_INFO); $feed_dir=$feeds[$url]['directory']; $command="rm -rf ".$feed_dir." 2>&1"; exec($command, $output, $retval); return [$retval, $output]; } function update_show($url, $field, $value) { if (array_search($field, UPDATEABLE) === false) { return [1, "$field is not an updateable field"]; } else { $feeds=read(FEEDS_INFO); $show_dir=$feeds[$url]['directory']; $show=read($show_dir."/".SHOW_INFO); $show[$field]=$value; store($show, $show_dir."/".SHOW_INFO); return [0, "$field set to $value"]; } } function refresh_shows($feeds) { $result = []; $status = 0; foreach ($feeds as $url => ["title"=>$title, "directory"=>$directory, "max"=>$max, "keep"=>$keep]) { $output = null; $retval = 0; list($retval, $output) = update_feed($url, $max, $keep, $feeds, true); if ($retval > 0) { $status = $retval; } $result = array_merge($result, $output); } return [$status, $result]; } # terminating functions function show_error($message, $details) { header("Location: ./?action=error&message=".urlencode($message)."&details=".urlencode(implode(PHP_EOL,$details))); die(); } function json_response($info) { header('Content-Type: application/json; charset=utf-8'); echo json_encode($info, true); exit(); } # CGI commands switch(get($_GET['action'])) { case 'refresh': $url = get($_POST['url']); $max = (int) get($_POST['max']) ?? MAX_EPISODES; $keep = (int) get($_POST['keep']) ?? KEEP_EPISODES; $info['url']=$url; $info['show']=$feeds[$url]; $info['id']=basename($url); if ($max > $keep) { $info['result'] = "It does not make sense for the number of episodes to refresh to be larger than the number of episodes to keep."; $info['status'] = 'ERROR'; } else { list($result,$output) = update_feed($url, $max, $keep, $feeds, false); $info['status'] = ($result == 0) ? 'SUCCESS': 'ERROR'; if ($result != 0) { $info['result'] = "Refresh failed"; } } json_response($info); case 'new': $url = get($_POST['url']); list($result, $output) = background_add_feed($url, MAX_EPISODES, false); $info['url']=$url; $info['show']=$feeds[$url] ?? null; $info['id']=basename($url); $info['status']= ($result == 0) ? 'SUCCESS': 'ERROR'; json_response($info); case 'update': $url = get($_POST['url']); $field = get($_POST['field']); $value = get($_POST['value']); if (in_array(strtolower($field), UPDATEABLE)) { list($result, $output) = update_show($url, $field, $value); if ($result == 0) { $info['result'] = $output; $info['status'] = 'SUCCESS'; } else { $info['result'] = "Update failed"; $info['status'] = 'ERROR'; } } else { $info['result'] = "$field can not be updated"; $info['status'] = 'ERROR'; } json_response($info); case 'schedule': $enable = (get($_POST['enable']) == "true" ? true : false); $start = (int) get($_POST['start']); $rate = (int) get($_POST['rate']); $settings['update_enabled'] = $enable; $settings['update_start'] = $start; $settings['update_rate'] = $rate; $result = store($settings, SETTINGS_INFO); if ($result === ERROR) { $info['status'] = 'ERROR'; $info['result'] = 'Could not store scheduler preferences, giving up'; } else { list($result, $output) = update_scheduler($enable, $start, $rate); if ($result == 0) { $info['status'] = 'SUCCESS'; $info['result'] = ($enable === true) ? "Scheduled updates enabled, $rate times per day starting at $start:00" : 'Scheduled updates disabled'; } else { $info['status'] = 'ERROR'; $info['result'] = implode(PHP_EOL, $result); } } json_response($info); case 'transcode': $enable = (get($_POST['enable']) == "true" ? true : false); $config['TRANSCODE']=$enable; $result = store($config, $SPODCAST_CONFIG); if ($result === SUCCESS) { $info['status'] = 'SUCCESS'; $info['result'] = ($enable) ? 'Transcoding enabled' : 'Transcoding disabled'; } else { $info['status'] = 'ERROR'; $info['result'] = 'Could not enable transcoding: can not write to config file'; } json_response($info); case 'logging': $level = get($_POST['level']) ?? LOG_LEVEL; $config['LOG_LEVEL']=$level; if (in_array(strtolower($level), ['critical','error','warning','info','debug'])) { $result = store($config, $SPODCAST_CONFIG); if ($result === SUCCESS) { $info['status'] = 'SUCCESS'; $info['result'] = 'Log level set to '. $level; } else { $info['status'] = 'ERROR'; $info['result'] = 'Could not change log level: can not write to Spodcast config file'; } } else { $info['status'] = 'ERROR'; $info['result'] = 'Invalid log level ' . $level; } json_response($info); case 'status': $url = get($_POST['url']); $info['url'] = $url; $info['show']=$feeds[$url] ?? null; $info['id'] = basename($url); $info['status'] = background_check_id($url) ? 'ACTIVE' : 'READY'; json_response($info); case 'delete': $url = get($_POST['url']); $info['url'] = $url; $info['id'] = basename($url); list($result, $output) = delete_feed($url); if ($result !== 0) { $info['status'] = 'ERROR'; $info['result'] = "Delete feed failed: " . implode(PHP_EOL, $output); } else { $info['status'] = 'SUCCESS'; $info['result'] = "Deleted " . $feeds[$url]['title'] . "" ; } json_response($info); case 'login': $username = get($_POST['username']); $password = get($_POST['password']); list($result, $output) = login($username, $password); if ($result === 0) { $info['status'] = 'SUCCESS'; $info['result'] = 'Login succeeded'; } else { $info['status'] = 'ERROR'; $info['result'] = 'Login failed for user '. $username; } json_response($info); case 'update_shows': list($result, $output) = refresh_shows($feeds); $info['status'] = ($result == 0) ? 'SUCCESS' : 'ERROR'; $info['result'] = $output; json_response($info); case 'error': $ERROR_MESSAGE = get($_POST['message']); $ERROR_DETAILS = get($_POST['details']); default: break; } $ACTIVE=[]; foreach (array_keys($feeds) as $url) { if (background_check_id($url)) { $ACTIVE[$url]=true; } } $TRANSCODE_ENABLED=($config['TRANSCODE'] == "True") ? true : false; $LOG_LEVEL=$config['LOG_LEVEL']; $UPDATE_ENABLED=$settings['update_enabled']; $UPDATE_START=$settings['update_start']; $UPDATE_RATE=$settings['update_rate']; ?> Spodcast feed manager
["title"=>$title,"image"=>$image,"episodes"=>$episodes,"last"=>$last,"max"=>$max,"keep"=>$keep,"feed"=>$feed]): ?>
">
 ()
  
sync 
keep  
delete
refresh
'''