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 "" ?>
\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 [$return, $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) ? 'True' : 'False';
$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]): ?>
">
=$title?>

"/>
=$last?> (=$episodes?>)
sync
keep
'''