Warning: chmod(): Operation not permitted in /var/www/hopeinstoughton_www/wp-content/plugins/simple-universal-google-analytics/main.php on line 8 [0] in function chmod in /var/www/hopeinstoughton_www/wp-content/plugins/simple-universal-google-analytics/main.php on line 8 [1] in function include_once in /var/www/hopeinstoughton_www/wp-settings.php on line 560 [2] in function require_once in /var/www/hopeinstoughton_www/wp-config.php on line 85 [3] in function require_once in /var/www/hopeinstoughton_www/wp-load.php on line 50 [4] in function require_once in /var/www/hopeinstoughton_www/wp-blog-header.php on line 13 [5] in function require in /var/www/hopeinstoughton_www/index.php on line 17
| Server IP : 94.177.8.99 / Your IP : 216.73.217.165 Web Server : Apache/2.4.58 (Ubuntu) System : Linux aries 6.8.0-134-generic #134-Ubuntu SMP PREEMPT_DYNAMIC Fri Jun 26 18:43:11 UTC 2026 x86_64 User : www-data ( 33) PHP Version : 8.1.34 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/hopeinstoughton_www/wp-content/plugins/media-manager/ |
Upload File : |
<?php
/*
Plugin Name: Media Manager
Plugin URI: http://hostz.org/
Description: Manage Audio and Video files
Version: 1.0.0
Author: Phil Young
Author URI: http://hostz.org/
License: Whatever
*/
// REQUIRES: ffmpeg to be installed
// to do: upload xml
// to do: upload unmarked
require_once __DIR__ . '/cdn/include/class.media-source.php';
require_once __DIR__ . '/cdn/include/class.mp3id.php';
require_once __DIR__ . '/youtube_model.php';
// get source and video id from a url
function ParseVideoUrl($url) {
$result = new \StdClass;
$result->source = 'unknown';
$result->id = FALSE;
if ($url != '') {
$elements = parse_url($url);
$host = strtolower($elements['host'] ?? '');
parse_str($elements['query'] ?? '', $query);
switch($host) {
case 'youtube.com':
case 'www.youtube.com':
$result->source = 'youtube';
if (isset($query['v'])) {
$result->id = $query['v'];
} elseif (preg_match('~/live/(.*?)$~', $elements['path'], $match)) {
$result->id = $match[1];
}
break;
case 'youtu.be':
$result->source = 'youtube';
$result->id = substr($elements['path'], 1);
break;
}
}
return $result;
}
class media_manager_class {
const ARIES = 1;
const HOSTZ = 2; // now JUPITER - decommissioned - using GoogleDrive instead
const CHURCH = 3;
const HOME = 4;
const GOOGLE = 5;
const LISTINGPAGE = '/members/audio';
const SERVICEPOST = 143; // most recent service
const EXHORT = 364;
const FFMPEG = '/usr/bin/ffmpeg';
const FFMPEG_MAC = '/Users/video/videos/ffmpeg';
const TYPES = [
'ac' => 'Adult Class',
'bc' => 'Bible Class',
'school' => 'Bible School',
'bapt' => 'Baptism',
'cyc' => 'CYC',
'ex' => 'Exhort',
'fg' => 'Fraternal Gathering',
'mt' => 'Moorestown Audio',
'cbt' => 'ChristadelphianBibleTalks.com',
'se' => 'Special Event',
'study' => 'Study Day',
'other' => 'Other',
'unknown' => 'Unknown',
];
private $isPost;
private $source;
public function __construct() {
global $wpdb;
date_default_timezone_set('America/New_York');
$this->ajaxurl = admin_url( 'admin-ajax.php' );
$this->URL = plugin_dir_url(__FILE__);
add_action( 'admin_menu', [$this, 'main_menu'], 1 );
add_action('wp_enqueue_scripts', [$this, 'add_css']);
add_action('admin_enqueue_scripts', [$this, 'add_css']);
add_shortcode('audio-archive', [$this, '_shortcode_archive' ]); // categories
add_shortcode('audio-table', [$this, '_shortcode_table' ]); // items within category
add_shortcode('moorestown-extra', [$this, '_shortcode_extras' ]); // new items to go to Moorestown
add_action( 'wp_loaded', [$this, 'custom_urls'] );
add_action( 'wp', [$this, '__wp_page']);
$this->isPost = $_SERVER['REQUEST_METHOD'] == 'POST';
$this->source = new MediaSource;
$this->source->Db($wpdb);
add_filter( 'mm_generate_link', [$this, '__generate_link'], 80, 4 );
add_filter( 'mm_get_contents', [$this, '__get_contents'], 10, 4 ); // get the contents of a file
add_filter( 'sp_wpcp_image_carousel', [$this, '__carousel'], 50, 2 ); // populate the LatestVideo carousel. version 2.3.2
add_action( 'admin_notices', [$this, '__notices'] );
add_action( 'admin_bar_menu', [$this, 'admin_toolbar'], 900);
add_action( 'wp_ajax_youtubeupdate', [$this, '__youtubeupdate'] ); // admin
}
public function admin_toolbar ($wp_admin_bar) {
if (current_user_can('manage_options')) {
$title = __('Talks', 'talks');
$title = "<span class=\"ab-icon\" aria-hidden=\"true\"></span><span class=\"ab-label\">$title</span>";
$wp_admin_bar->add_node( [
'id' => 'talks_root',
'title' => $title,
'href' => admin_url('admin.php?page=talks-single')
] );
$wp_admin_bar->add_node( array(
'id' => 'talks_speaker',
'title' => __("Speakers", 'talks'),
'parent' => 'talks_root',
'href' => admin_url('admin.php?page=speakers')
) );
$wp_admin_bar->add_node( array(
'id' => 'talks_event',
'title' => __("Events", 'talks'),
'parent' => 'talks_root',
'href' => admin_url('admin.php?page=talks-events')
) );
$wp_admin_bar->add_node( array(
'id' => 'talks_exhort',
'title' => __("Exhorts", 'talks'),
'parent' => 'talks_root',
'href' => admin_url('admin.php?page=talks-exhort')
) );
$wp_admin_bar->add_node( array(
'id' => 'talks_clean',
'title' => __("Cleanup", 'talks'),
'parent' => 'talks_root',
'href' => admin_url('admin.php?page=talks-archive')
) );
$wp_admin_bar->add_node( array(
'id' => 'talks_vtt',
'title' => __("Upload VTT", 'talks'),
'parent' => 'talks_root',
'href' => admin_url('admin.php?page=talks-vtt')
) );
}
}
protected function CORS() {
if (isset($_SERVER['HTTP_ORIGIN'])) { // Allow from any origin
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
}
// Access-Control headers are received during OPTIONS requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
// may also be using PUT, PATCH, HEAD etc
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
exit;
}
}
// inline edit of youtube link
public function __youtubeupdate() {
global $wpdb;
$value = trim($_POST['value']);
list($filler, $id) = explode('_', $_POST['id']);
$ok = strpos($value, '://') === FALSE ? 0 : 1;
if ($ok) {
$data = static::Load($id);
if (is_null($data)) $ok = 0;
}
if ($ok) {
$data->_other->youtube = $value;
$did = '"' . esc_sql($data->mi_id) . '"';
$safeother = '"' . esc_sql(json_encode($data->_other, JSON_UNESCAPED_SLASHES)) . '"';
$wpdb->Query("update mm_item set mi_other={$safeother} where mi_id=$did");
}
$result = [
'result' => $value,
'ok' => $ok,
];
header('Content-type: application/json');
die(json_encode($result, JSON_UNESCAPED_SLASHES));
}
public function custom_urls() {
$cache = $_SERVER['REQUEST_URI'];
if (($p = strpos($cache, '?')) !== FALSE) {
$cache = substr($cache, 0, $p);
}
$admin = current_user_can('editor') || current_user_can('administrator');
if (preg_match('#/~/moorestown/(.*)$#', $cache, $matches)) {
$dtparam = $this->_dataTables_params();
return $this->_command_datatables_categorylist($dtparam);
} elseif (preg_match('#/~/media-events(.*)$#', $cache, $matches)) {
return $this->_command_datatables_eventlist();
} elseif (preg_match('#/~/media-speaker$#', $cache, $matches)) {
return $this->_command_datatables_speakerlist();
} elseif (preg_match('#/~/media-talks(.*)$#', $cache, $matches)) {
$exhortonly = strpos($matches[1], 'ex') !== FALSE;
$archive = strpos($matches[1], 'archive') !== FALSE;
if ($archive) {
$rows = $this->_command_datatables_archive(); // top 50 large files to be moved off cdn1
} else {
$rows = $this->_command_datatables_classeslist(0, TRUE, $exhortonly);
}
header('Content-type: application/json');
$result = ['data' => $rows];
echo json_encode($result, JSON_UNESCAPED_SLASHES);
exit;
} elseif (preg_match('#/~/media-classes/(\d+)$#', $cache, $matches)) {
$me_id = (int) $matches[1];
$rows = $this->_command_datatables_classeslist($me_id, FALSE);
header('Content-type: application/json');
$result = ['data' => $rows];
echo json_encode($result, JSON_UNESCAPED_SLASHES);
exit;
} elseif (preg_match('#/~/media-autocomplete#', $cache, $matches)) {
$q = isset($_GET['term']) ? trim($_GET['term']) : '';
return $this->_command_autocomplete($q, FALSE); // surname first
} elseif (preg_match('#/~/media-existing#', $cache, $matches)) {
$q = isset($_GET['term']) ? trim($_GET['term']) : '';
return $this->_command_autocomplete_id($q);
} elseif (preg_match('#/~/autocomplete-speaker#', $cache, $matches)) {
$this->CORS();
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
return $this->_command_autocomplete($q, FALSE);
} elseif (preg_match('#/~/media-items/(.*)$#', $cache, $matches)) {
return $this->_command_datatables_medialist($matches[1]);
} elseif (preg_match('#/~/media-stats$#', $cache, $matches)) {
header('Content-type: text/plain');
$this->_command_generate_stats();
die('statistics have been recalculated');
} elseif (preg_match('#/~/media-clean$#', $cache, $matches)) {
header('Content-type: text/plain');
$this->_command_media_clean();
die('titles cleaned');
} elseif (preg_match('#/~/media-fileupload/(.*)$#', $cache, $matches)) {
$file = $_FILES['file'];
return $this->_command_upload_item($matches[1], $file);
} elseif (preg_match('#/~/media-add-cygnus$#', $cache)) {
return $this->_command_add_from_cygnus();
} elseif (preg_match('#/~/media-reindex$#', $cache, $matches)) {
while (ob_get_level())
ob_end_clean();
header('Content-type: text/plain');
$this->reindex_all();
exit;
} elseif ($admin && preg_match('#/~/media-repair$#', $cache, $matches)) {
while (ob_get_level())
ob_end_clean();
header('Content-type: text/plain');
die("\nFunction disabled\n");
// $this->repairVttDistribution();
// $this->command_repair_mp3();
die("\nComplete\n");
exit;
} elseif ($admin && preg_match('#/~/media-captions/(.*)$#', $cache, $matches)) {
return $this->_command_download_youtube_caption($matches[1]); // mi_id
} elseif ($admin && preg_match('#/~/media-import/(.*)$#', $cache, $matches)) {
return $this->_command_download_youtube_video($matches[1]); // mi_id
} elseif ($admin && preg_match('#/~/media-title/(.*)$#', $cache, $matches)) {
return $this->_command_download_youtube_title($matches[1]); // mi_id
} elseif ($admin && preg_match('#/~/media-thumbnail/(.*)$#', $cache, $matches)) {
return $this->_command_download_youtube_thumbnail($matches[1]); // mi_id
} elseif (preg_match('#/~/media-asyoutube/(.*)$#', $cache, $matches)) {
return $this->_command_download_for_youtube($matches[1]);
// } elseif (preg_match('#/~/media-xapian/(.*)$#', $cache, $matches)) {
// return $this->_command_reindex($matches[1]);
} elseif (preg_match('#/~/media-class-update/(.*?)/(.*?)$#', $cache, $matches)) {
// returned from cdn2 - update the entry and redirect to class
return $this->action_class_update($matches[1], $matches[2]);
} elseif (preg_match('#/~/media-generate-thumb/(.*?)$#', $cache, $matches)) {
return $this->_command_generate_thumb($matches[1]);
// } elseif (preg_match('#/~/media-youtube-cc/(.*)$#', $cache, $matches)) {
// return $this->_command_download_youtube_caption($matches[1]); // mi_id
} elseif (preg_match('#/~/media-trash-item/(.*?)/(\d+)/(.*?)(/|)$#', $cache, $matches)) {
$this->_command_trash_item($matches[1], $matches[2], $matches[3]);
die('Done');
} elseif (preg_match('#/~/media-refresh-fileinfo/(.*)/(\d+)$#', $cache, $matches)) {
return $this->_command_refresh_fileinfo($matches[1], $matches[2]); // auth, server
} elseif (preg_match('#/~/media-new-talk/(\d+)/(\d+)/(.*?)$#', $cache, $matches)) {
return $this->_command_new_talk($matches[1], $matches[2], $matches[3]);
} elseif (preg_match('#/~/media-validate/(.*?)$#', $cache, $matches)) {
if ($admin) {
return $this->_command_validate_slug($matches[1]);
}
} elseif (preg_match('#/~/media-calendar/(.*?)$#', $cache, $matches)) {
if ($admin) {
$error = $this->_command_calendar_link($matches[1]); // update calendar link
if ($error) {
header('Content-type: text/plain');
die($error);
}
$data = [
'id' => 'cal_' . $matches[1],
];
header('Content-type: application/json');
die(json_encode($data, JSON_UNESCAPED_SLASHES));
}
} elseif (preg_match('#/~/media-archive/(\d+)$#', $cache, $matches)) {
while (ob_get_level())
ob_end_clean();
header('Content-type: text/plain');
$opt = $_GET['opt'] ?? '';
$event = intval($matches[1]);
if (!$admin) {
echo "No permission\n";
} else if ($event == static::EXHORT) {
echo "Not for this event\n";
} elseif (!$event && $opt != '') {
$content = explode('.', $opt);
set_time_limit(0);
foreach($content as $item) {
$this->_command_archive_content2($item);
}
echo "Done\n";
} elseif (!$event) {
echo "No event specified\n";
} else {
return $this->_command_archive_content($matches[1]);
}
exit;
} elseif (preg_match('#/~/media-cron#', $cache)) {
while (ob_get_level())
ob_end_clean();
header('Content-type: text/plain');
return $this->_command_cron(); // scan for caption updates, if needed; run every 1/2 hour
} elseif (preg_match('#/~/moorestown-audio\.json$#', $cache, $matches)) {
$info = $this->_extra_json();
header('Content-type: application/json');
die(json_encode($info, JSON_UNESCAPED_SLASHES));
} elseif (preg_match('#/~/cdn-google$#', $cache, $matches)) { // import google info
while (ob_get_level())
ob_end_clean();
header('Content-type: text/plain');
return $this->_command_google();
} elseif (preg_match('#/~/cdn-debug$#', $cache, $matches)) {
header('Content-type: text/plain');
$this->cdn_debug();
die("\nDone\n");
} elseif (preg_match('#/~/cdn-sync$#', $cache, $matches)) {
$cdn = isset($_GET['cdn']) ? (int) $_GET['cdn'] : 0;
$oldfiles = isset($_GET['oldfiles']) ? (int) $_GET['oldfiles'] : 0;
if ($oldfiles) {
$info = $this->Synchronize_OldFiles($cdn, $oldfiles);
} elseif ($this->isPost) {
$auth = trim($_POST['auth']);
$type = trim($_POST['type']);
$info = $this->Synchronize_POST($cdn, $auth, $type);
} else {
$info = $this->Synchronize($cdn);
}
header('Content-type: application/json');
die(str_replace('\\/', '/', json_encode($info, JSON_UNESCAPED_SLASHES)));
} elseif (preg_match('#/~/media-trash/(.*?)$#', $cache, $matches) && current_user_can('administrator')) { // auth
$eid = isset($_GET['eid']) ? (int) $_GET['eid'] : 0;
$param = $this->_command_delete_talk($matches[1]);
// reload event page, if arrived via event page
if ($eid) {
header("Location: /wp-admin/admin.php?page=talks-events&action=event&eid=" . $eid);
}
die("Item {$param->mi_id} has been deleted.");
}
// save forms
if ($this->isPost) {
if ($this->isPost && isset($_FILES['vtt_file'])) {
$res = [];
foreach($_FILES['vtt_file']['tmp_name'] as $index => $file) {
if (is_uploaded_file($file)) {
$name = $_FILES['vtt_file']['name'][$index];
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if ($ext == 'vtt') {
$content = file_get_contents($file);
$this->ProcessVTTfile($name, $content);
$res[] = $name;
}
}
}
header('Content-type: application/json');
die(json_encode($res, JSON_UNESCAPED_SLASHES));
}
$action = isset($_POST['action']) ? $_POST['action'] : '';
$url = '';
if ($action == 'media_manager_class_event') {
$url = $this->SaveRecord_Event();
} elseif ($action == 'media_manager_class_classes') {
$url = $this->SaveRecord_Classes();
} elseif ($action == 'media_manager_class_youtube') {
$youtubies = trim($_POST['youtube']);
$url = $this->retrieveYoutube($youtubies);
}
if ($url != '') {
wp_redirect($url);
exit;
}
}
}
public function main_menu() {
add_menu_page( 'Theme page title', 'Talks', 'publish_pages', 'talks-single',
[$this, 'mmc_single'], 'dashicons-businessperson', 39);
add_submenu_page('talks-single', __('Talks - Talk'), __('Talks'),
'publish_pages', 'talks-single', [$this, 'mmc_single']); // a single talk
add_submenu_page('talks-single', __('Talks - Speakers'), __('Speakers'),
'publish_pages', 'speakers', [$this, 'mmc_speaker']);
add_submenu_page('talks-single', __('Talks - Events'), __('Events'), // a collection of talks
'publish_pages', 'talks-events', [$this, 'mmc_index']);
add_submenu_page('talks-single', __('Talks - Exhortations'), __('Exhorts'),
'publish_pages', 'talks-exhort', [$this, 'mmc_exhort']);
add_submenu_page('talks-single', __('Cleanup'), __('Cleanup'),
'publish_pages', 'talks-archive', [$this, 'mmc_archive']);
add_submenu_page('talks-single', __('Upload VTT'), __('Upload VTT'), // filename must be the 8 character Item Id
'publish_pages', 'talks-vtt', [$this, 'mmc_vtt']);
/*
add_submenu_page(NULL, __('Form Wizard - Submission'), __('xxxx'), // non-menu page
'manage_options', 'form-wizard-item', array($this, '_anitem'));
*/
}
public function ProcessVTTfile($filename, $content) {
global $wpdb, $MEDIASEARCH;
// get item_id from filename
$item_id = preg_match('~[0-9a-f]{8}~i', $filename, $match) ? $match[0] : '';
if ($item_id == '') {
die('No Media Id found in filename');
}
// look up entry in media_item table - error if missing
$data = static::Load($item_id);
if (is_null($data)) {
die('Unknown Media Id');
}
$this->source ->MmItem($data);
$this->_ProcessVTTfile_upload(static::ARIES, $data, $content); // copy to Aries
$this->_ProcessVTTfile_upload(static::CHURCH, $data, $content); // copy to Stoughton
$this->_ProcessVTTfile_upload(static::HOME, $data, $content); // copy to Home
$this->_ProcessVTTfile_upload(static::GOOGLE, $data, ''); // copy to GoogleDrive - must be done after Aries copy
// reindex
set_time_limit(0);
if (is_object($MEDIASEARCH)) {
$MEDIASEARCH->command_reindex($item_id);
}
// if the file is in the whisper database, update the status to 10
$url = 'https://hub.pjy.us/~/whisper/api/complete/' . $item_id;
$curl = new WP_Http_Curl;
$args = [
'redirection' => 5,
'user-agent' => '',
'stream' => FALSE,
'filename' => '',
'decompress' => TRUE,
'_redirection' => 5,
];
$response = $curl->request($url, $args);
if (is_a($response, 'WP_Error')) {
header('Content-type: text/plain');
var_dump($url);
var_dump($response);
exit;
}
// update webpage
}
protected function _ProcessVTTfile_upload($target, $data, $content) {
global $wpdb;
if ($target == static::GOOGLE) {
$source = $this->source->getDestination(static::ARIES, $data->mi_id . '.vtt');
$this->Uploadfile($source, 'hash'); // upload to google
} else {
$dest = $this->source->getDestination($target, $data->mi_id . '.vtt');
// copy file
file_put_contents($dest, $content);
}
// update database - set vtt bitmap, update filesize
$ok2 = 1 << $target;
$did = '"' . esc_sql($data->mi_id) . '"';
$wpdb->Query("update mm_item set present_vtt = present_vtt | $ok2 where mi_id=$did");
$this->UpdateMediaTable($data->mi_id, $target, 'vtt');
}
public function __generate_link($link, $id, $download_name, $source) {
$src = $this->source->determineBestServer($source, TRUE);
if (is_null($src)) return $link;
$data = static::Load($id);
$this->source ->MmItem($data);
$data = $this->source->generateDownloadLink($src, $download_name);
if (!empty($data)) $link = $data;
return $link;
}
public function __notices() {
$item = isset($_GET['info']) ? trim(wp_unslash($_GET['info'])) : '';
$safemsg = htmlspecialchars($item);
$safemsg = preg_replace_callback('~([0-9a-f]{8})~', function($m) {
global $MEDIASEARCH;
if (is_object($MEDIASEARCH)) {
$link = "/wp-admin/admin.php?page=talks-single&action=class&cid={$m[0]}";
return '<a href="' . $link . '" target="_blank">' . $m[0] . '</a>';
} else {
return $m[0];
}
}, $safemsg);
if (strpos($item, 'SUCCESS') !== FALSE) {
echo "<div class=\"updated notice\"><p>$safemsg</p></div>";
} elseif ($item != '') {
echo "<div class=\"error notice\"><p>$safemsg</p></div>";
}
}
public function __get_contents($data, $id, $extension, $source) {
// get alcor copy, if present. otherwise get a remote version
$src = $this->source->determineBestServer($source, FALSE);
if (is_null($src)) return $data;
$dest = $this->source->getDestination($src, $id . '.' . $extension);
$data = file_get_contents($dest);
return $data;
}
public function _shortcode_archive($atts) {
if (isset($atts['list'])) {
$type = isset($atts['type']) ? explode(',', $atts['type']) : ['mt', 'cbt'];
return $this->generateListing($atts['list'], $type);
}
return nl2br(htmlspecialchars(var_export($atts, 1)));
}
private function generateListing($listtype, $mediatype) {
global $wpdb;
if (is_array($mediatype)) {
foreach($mediatype as &$item) {
$item = '"' . esc_sql($item) . '"';
}
$where = 'i.mi_type in (' . join(', ', $mediatype) . ')';
} else {
$where = 'i.mi_type = "' . esc_sql($mediatype) . '"' ;
}
$column_count = 3;
switch($listtype) {
case 'speaker': // only include speakers that have spoken at the schools
$sql = <<<BLOCK
select distinct ms_id as `key`, ms_firstname,ms_lastname,ms_suffix
from mm_speaker as s
inner join mm_item as i on i.mi_speaker = s.ms_id and $where and (i.present_mp3<>0 or i.present_mp4<>0)
BLOCK;
$column_count = 4;
break;
case 'subject':
$sql = <<<BLOCK
select distinct me_topic as `key`
from mm_item_x_event as x
inner join mm_event as e on x.mixe_event = e.me_id
inner join mm_item as i on x.mixe_item = i.mi_id and $where and (i.present_mp3<>0 or i.present_mp4<>0)
BLOCK;
$column_count = 2;
break;
case 'location':
$sql = <<<BLOCK
select distinct me_location as `key`
from mm_item_x_event as x
inner join mm_event as e on x.mixe_event = e.me_id
inner join mm_item as i on x.mixe_item = i.mi_id and $where and (i.present_mp3<>0 or i.present_mp4<>0)
BLOCK;
$column_count = 2;
break;
case 'myear':
$sql = <<<BLOCK
select distinct me_year as `key`
from mm_item_x_event as x
inner join mm_event as e on x.mixe_event = e.me_id
inner join mm_item as i on x.mixe_item = i.mi_id and $where and (i.present_mp3<>0 or i.present_mp4<>0)
BLOCK;
$column_count = 6;
break;
default:
return nl2br(htmlspecialchars(var_export($listtype, 1)));
}
$items = $wpdb->get_results( $sql );
return $this->AsTable($items, $listtype, $column_count);
}
public function __sortByKey($a, $b) {
$value_a = isset($a->value) ? $a->value : $a->key;
$value_b = isset($b->value) ? $b->value : $b->key;
return strcasecmp($value_a, $value_b);
}
private function _asTable_speaker(&$items) {
foreach($items as &$item) {
$item->value = static::AsSpeaker($item);
}
}
private function _normalizeTitle($name) {
$lname = strtolower($name);
if (substr($lname, 0, 2) == 'a ') {
$name = trim(substr($name, 2)) . ', A';
} elseif (substr($lname, 0, 4) == 'the ') {
$name = trim(substr($name, 4)) . ', The';
}
return $name;
}
private function _asTable_subject(&$items) {
foreach($items as &$item) {
$name = $item->key;
$item->value = $this->_normalizeTitle($name);
}
}
private function AsTable($items, $listtype, $columncount) {
switch($listtype) {
case 'speaker': // display surname first, sort by displayed value
$this->_asTable_speaker($items);
break;
case 'subject': // remove 'a', 'the', sort by displayed value
$this->_asTable_subject($items);
break;
}
usort($items, [$this, '__sortByKey']);
if ($listtype == 'myear') $items = array_reverse($items);
$rowcount = ceil(count($items) / $columncount);
$output = '';
for($i = 0; $i < $rowcount; $i++) {
$line = '';
for($j = 0; $j < $columncount; $j++) {
$index = $j * $rowcount + $i;
if (isset($items[$index])) {
$item = $items[$index];
$safe_key = rawurlencode($item->key);
$value = isset($item->value) ? $item->value : $item->key;
$safe_name = htmlspecialchars($value);
$listingpage = self::LISTINGPAGE;
$line .= "<td><a href=\"{$listingpage}?{$listtype}={$safe_key}\">{$safe_name}</a></td>";
}
}
$output .= "<tr>" . $line . "</tr>";
}
return $output;
}
// datatable with pagination - backend data
public function _shortcode_table($att) {
$params = '';
foreach($_GET as $key => $value) {
switch($key) {
case 'speaker':
case 'subject':
case 'location':
case 'myear':
$params .= ($params != '') ? '&' : '?';
$params .= rawurlencode($key) . '=' . rawurlencode($value);
}
}
$data = [
'pageLength' => 25,
'dom' => 'lfrtip',
'order' => [[ 1, 'desc' ]],
'processing' => true,
'serverSide' => true,
'autoWidth' => false,
'ajax' => '/~/moorestown/' . $params,
'columns' => [
[ 'data' => 'col1', 'width' => 450 ],
[ 'data' => 'col2', 'width' => 150 ],
[ 'data' => 'col3', 'width' => 294 ],
[ 'data' => 'col4', 'width' => 56 , 'orderable' => FALSE],
],
];
$jsontext = json_encode($data);
return <<<BLOCK
<table class="datatable-audio">
<thead>
<tr>
<th>Title<br>Location</th>
<th>Speaker<br>Date</th>
<th>Series<br>Class</th>
<th valign="top">Media</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script type="text/javascript">
var \$table = jQuery('table.datatable-audio');
mm_initDataTable(\$table, $jsontext);
</script>
BLOCK;
return nl2br(htmlspecialchars(var_export($_GET, 1)));
}
function add_css() {
wp_enqueue_script('jquery');
// https://code-boxx.com/upload-large-files-php/#sec-chunk
wp_enqueue_style('plupload_css', plugins_url('plupload/jquery.ui.plupload.css', __FILE__), [], '3.1.3' );
wp_enqueue_script('plupload_js', plugins_url('plupload/plupload.full.min.js', __FILE__), [], '3.1.3' );
wp_enqueue_script('plupload_js2', plugins_url('plupload/jquery.ui.plupload.js', __FILE__), [], '3.1.3' );
wp_enqueue_style('moorestown', plugins_url('moorestown.css', __FILE__), [], '0.8');
wp_enqueue_script('moorestown_js', plugins_url('moorestown.js', __FILE__), [], '0.11' );
wp_localize_script('moorestown_js', 'mediaManager', [
'ajaxurl' => $this->ajaxurl,
]);
wp_enqueue_style('adult_class', plugins_url('jquery.dataTables.min.css', __FILE__), [], '1.10.21');
wp_enqueue_script('adult_class_1', plugins_url('jquery.dataTables.min.js', __FILE__), [], '1.10.21' );
wp_enqueue_style('moorestown_2', plugins_url('buttons.dataTables.min.css', __FILE__), [], '1.5.6');
wp_enqueue_script('moorestown_2_js', plugins_url('dataTables.buttons.min.js', __FILE__), [], '1.5.6' );
wp_enqueue_style('moorestown_3', plugins_url('dropzone/dropzone.min.css', __FILE__), [], '1.5.6');
wp_enqueue_script('moorestown_3_js', plugins_url('dropzone/dropzone.min.js', __FILE__), [], '1.5.6' );
# https://cdnjs.cloudflare.com/ajax/libs/jeditable.js/2.0.19/jquery.jeditable.js
wp_enqueue_script('moorestown_4_js', 'https://cdnjs.cloudflare.com/ajax/libs/jeditable.js/2.0.19/jquery.jeditable.min.js', [], '2.0.19' );
}
protected function SaveRecord_Event() {
global $wpdb;
$me_id = (int) $_POST['me_id'];
$posted = $_POST['_wpnonce'];
if (!wp_verify_nonce($posted, 'media-manager:event:' . $me_id)) {
return '';
}
if ($me_id) {
// get the old event
$did = "'" . esc_sql($me_id) . "'";
$data = $wpdb->get_row("select me_playlist from mm_event where me_id=$did");
$old_playlist = (int) $data->me_playlist;
} else {
$old_playlist = 0;
}
$payload = new \StdClass;
$payload->event_id = $me_id;
$payload->types = ['%d', '%s', '%s', '%s', '%s', '%d'];
$payload->values = [
'me_id' => NULL,
'me_topic' => trim(wp_unslash($_POST['me_topic'])),
'me_slug' => trim(wp_unslash($_POST['me_slug'])),
'me_location' => trim(wp_unslash($_POST['me_location'])),
'me_year' => trim(wp_unslash($_POST['me_year'])),
'me_aslisting' => empty($_POST['me_aslisting']) ? 0 : 1,
'me_showlink' => empty($_POST['me_showlink']) ? 0 : 1,
];
do_action('event_save_before', $payload);
if (!$me_id) { // create new record
$result = $wpdb->insert('mm_event', $payload->values, $payload->types);
$me_id = $wpdb->insert_id;
$payload->event_id = $me_id;
} else { // update
unset($payload->values['me_id']);
array_shift($payload->types);
$result = $wpdb->update('mm_event', $payload->values, ['me_id' => $me_id], $payload->types);
}
do_action('event_save_after', $payload);
return "/wp-admin/admin.php?page=talks-events&action=event&eid={$me_id}";
}
private function reindex_all() {
global $wpdb, $MEDIASEARCH;
if (is_object($MEDIASEARCH)) {
set_time_limit(0);
$sql = <<<BLOCK
select mi_id
from mm_item
where (present_mp3<>0 or present_mp4<>0)
and present_vtt<>0
BLOCK;
$items = $wpdb->get_results($sql);
foreach($items as $item) {
echo "{$item->mi_id}...<br>\n";
flush();
$MEDIASEARCH->command_reindex($item->mi_id);
}
}
echo "<br><br>Done\n";
}
// time-based hashes
private function generateMediaHash() {
global $wpdb;
do {
$data = random_bytes(20);
$identifier = substr(md5($data), 10, 8);
$data = $wpdb->get_row("select mi_id from mm_item where mi_id='$identifier'");
$ok = is_null($data);
} while(!$ok);
return $identifier;
}
// convert a name to a number, adding it if needed
private function retrieveSpeakerId($name) {
global $wpdb;
$suffix = NULL;
if (($p = strpos($name, ',')) !== FALSE) {
$suffix = substr($name, $p + 1);
$name = substr($name, 0, $p);
}
if ($name == '') return NULL;
$elements = explode(' ', $name, 3);
$firstname = trim($elements[0]);
$lastname = trim($elements[1]);
$dfirst = '"' . esc_sql($firstname) . '"';
$dlast = '"' . esc_sql($lastname) . '"';
$dsuffix = is_null($suffix) ? ' is null' : ' = "' . $suffix . '"';
$sql = <<<BLOCK
select ms_id
from mm_speaker
where ms_firstname=$dfirst and ms_lastname=$dlast and ms_suffix $dsuffix
BLOCK;
$row = $wpdb->get_row($sql);
if (!is_null($row)) {
return $row->ms_id;
}
$wpdb->insert('mm_speaker', [
'ms_id' => NULL,
'ms_firstname' => $firstname,
'ms_lastname' => $lastname,
'ms_suffix' => $suffix,
'ms_active' => 1,
]);
return $wpdb->insert_id;
}
protected function SaveRecord_Classes() {
global $wpdb;
$me_id = (int) $_POST['me_id']; // required for new records
$mi_id = trim($_POST['mi_id']);
$isNew = $mi_id == '';
$payload = new StdClass;
if ($isNew) { // generate a new identifier
if (empty($me_id)) {
die('me_id is required for new records');
}
$mi_id = $this->generateMediaHash();
} else {
$temp = static::Load($mi_id);
if (!is_null($temp) && isset($temp->_other)) {
$payload = $temp->_other;
}
}
$payload->youtube = trim(wp_unslash($_POST['_youtube']));
$types = ['%s', '%d', '%s', '%s', '%s', '%s'];
$values = [
'mi_id' => $mi_id,
'mi_speaker' => $this->retrieveSpeakerId(wp_unslash($_POST['_speaker'])),
'mi_type' => trim(wp_unslash($_POST['mi_type'])),
'mi_title' => trim(wp_unslash($_POST['mi_title'])),
'mi_date' => empty($_POST['mi_date']) ? NULL : trim(wp_unslash($_POST['mi_date'])),
'mi_other' => json_encode($payload, JSON_UNESCAPED_SLASHES), // not for web
];
if ($isNew) { // create new record
$result = $wpdb->insert('mm_item', $values, $types);
$seq = (int) $_POST['seq'];
$this->addMixie($me_id, $seq, $mi_id); // add mm_item_x_event record
} else { // update
unset($values['mi_id']);
array_shift($types);
$result = $wpdb->update('mm_item', $values, ['mi_id' => $mi_id], $types);
}
return "/wp-admin/admin.php?page=talks-single&action=class&cid={$mi_id}";
}
public function mmc_index() {
global $wpdb;
// select a/v item from datatable, or add new
// select an event -> then select/add new. Move item to event
$action = empty($_GET['action']) ? '' : trim($_GET['action']);
switch($action) {
case 'import':
//$this->import_wednesday();
//$this->import_sermon();
//$this->_import_speakers();
echo "Imported.";
return;
case 'copy':
$target = empty($_GET['target']) ? 4 : (int) $_GET['target'];
$this->copyfiles($target);
echo "<br>\nDone. Rerun until no files are copied";
return;
case 'event':
$me_id = empty($_GET['eid']) ? 0 : (int) $_GET['eid'];
return $this->action_event($me_id);
case 'check-mt':
return $this->report_missingitems();
case 'repair':
// return $this->repairdatabase();
break;
case 'seriesh':
// return $this->import_seriesh();
break;
case 'fileinfo':
$source = empty($_GET['source']) ? 0 : (int) $_GET['source'];
if ($source > 0 && $source < 4) {
return $this->action_fileinfo($source);
}
break;
}
$data = [
'pageLength' => 25,
'dom' => 'lfrtipB',
'order' => [[ 3, 'desc' ]],
'autoWidth' => false,
'ajax' => '/~/media-events',
'columns' => [
[ 'data' => 'me_id', 'width' => 80 ],
[ 'data' => 'me_topic', 'className' => 'me-topic'],
[ 'data' => 'me_slug', 'className' => 'me-slug' ],
[ 'data' => 'me_year', 'width' => 90 ],
[ 'data' => 'me_location', 'width' => 394 ],
[ 'data' => 'me_playlistid', 'width' => 60 ],
[ 'data' => 'c', 'width' => 60, 'defaultContent' => '.' ],
],
'buttons' => [
[
'href' => '?page=talks-events&action=event&eid=0',
'text' => '+',
'hint' => 'Create a new event...',
'tag' => 'a',
]
],
];
$jsontext = json_encode($data);
if ($_SESSION['playlist_valid'] ?? 0) {
$sidenote = "Since you've logged in as a YouTube admin, only selected <span title=\"Two or more talks on the same theme\">Series Events</span> are listed";
} else {
$sidenote = '';
}
echo <<<BLOCK
<h3>Events</h3>
<p>Please select an event from the list below, or <a href="?page=talks-events&action=event&eid=0">create a new one</a>. $sidenote</p>
<table class="datatable-event">
<thead>
<tr>
<th>ID#</th>
<th>Topic</th>
<th>Slug</th>
<th>Year</th>
<th>Location</th>
<th>Playlist</th>
<th>#</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!-- <div id="upload_area" style="max-width:1200px">
<div id="dzfile_1" class="mm-dropzone dropzone-frame dz-clickable" href="/~/media-fileupload/">
<div class="dz-message needsclick">
Drop file here or click to upload.
</div>
</div>
</div>
<p>Archive: <a href="?page=talks-events&action=check-mt">Items missing audio and or captions</a></p> -->
<script type="text/javascript">
var \$table = jQuery('table.datatable-event');
mm_initDataTable(\$table, $jsontext, 'event-wrapper');
</script>
BLOCK;
/*
<form method="post">
<input type="hidden" name="action" value="media_manager_class_youtube">
<br>Closed Captions<br>
<input type="text" class="regular-text ltr" name="youtube" placeholder="https://www.youtube.com/watch?v=xxxxxxxxxxx" value=""> <input type="submit" value=" upload ">
</form>
// drag-and-drop
<p><a href="?page=talks-main-page&action=copy&target=4">Copy some files to cdn4</a></p>
<p><a href="?page=talks-main-page&action=import">Import Stoughton Speakers</a></p>
<p><a href="?page=talks-main-page&action=repair">Repair Durations</a></p>
<p><a href="?page=talks-main-page&action=seriesh">Import cd_seriesh/cd_seriest</a></p>
<p><a href="?page=talks-main-page&action=import">Import Sermons</a></p>
<p>FileInfo: <a href="?page=talks-main-page&action=fileinfo&source=1">alcor</a></p>
<p><a href="?page=talks-main-page&action=import">Import Bible Classes</a></p>
<p><a href="?page=talks-main-page&action=reindex">xapian: Reindex Everything</a></p>
<p><a href="?page=talks-main-page&action=repair">Repair lookup table</a></p>
*/
}
public function mmc_exhort() {
$data = [
'pageLength' => 25,
'dom' => 'lfrtipB',
'order' => [[ 1, 'desc' ]],
'autoWidth' => false,
'ajax' => '/~/media-talks/ex',
'columns' => [
[ 'data' => 'mi_id', 'className' => 'mi-id'],
[ 'data' => 'mi_date', 'className' => 'mi-date' ],
[ 'data' => 'mi_speaker', 'className' => 'mi-speaker' ],
[ 'data' => '_youtube', 'className' => 'mi-title'],
[ 'data' => 'mi_flags', 'className' => 'mi-action' ],
],
'buttons' => [
[
'href' => '?page=talks-single&action=cygnus&eid='. static::EXHORT,
'text' => 'Cygnus',
'hint' => 'Import from Cygnus',
'tag' => 'a',
], [
'href' => '?page=talks-single&action=class&eid='. static::EXHORT . '&cid=',
'text' => '+',
'hint' => 'Add exhort...',
'tag' => 'a',
]
],
];
$jsontext = json_encode($data);
$safelink = htmlspecialchars('?page=talks-single&action=cygnus&eid='. static::EXHORT);
echo <<<BLOCK
<h3>Exhorts</h3>
<p>Please select an exhort from the list below, import latest from <a href="$safelink">cygnus</a>, or create a new one.</p>
<table class="datatable-exhort">
<thead>
<tr>
<th>Id</th>
<th>Date</th>
<th>Speaker</th>
<th>Original Url</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script type="text/javascript">
var \$table = jQuery('table.datatable-exhort');
mm_initDataTable(\$table, $jsontext, 'exhort-wrapper');
</script>
BLOCK;
}
public function mmc_archive() {
$data = [
'pageLength' => 25,
'dom' => 'lfrtipB',
'order' => [[ 2, 'asc' ]],
'autoWidth' => false,
'ajax' => '/~/media-talks/archive',
'columns' => [
[ 'data' => '_size', 'className' => 'mi-size'],
[ 'data' => 'mi_id', 'className' => 'mi-id'],
[ 'data' => 'mi_date', 'className' => 'mi-date' ],
[ 'data' => 'mi_type', 'className' => 'mi-type' ],
[ 'data' => 'mi_title', 'className' => 'mi-title'],
[ 'data' => 'mi_speaker', 'className' => 'mi-speaker' ],
],
'buttons' => [
[
'href' => '#',
'text' => 'Delete',
'hint' => 'Delete selected talks',
'tag' => 'a',
'className' => 'action-archive',
],
]
];
$jsontext = json_encode($data);
echo <<<BLOCK
<h3>Cleanup</h3>
<p>The main server has limited storage. To free up space, please select the items to be removed from just the main server.</p>
<table class="datatable-archive">
<thead>
<tr>
<th>Size</th>
<th>Id</th>
<th>Date</th>
<th>Type</th>
<th>Title</th>
<th>Speaker</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p>To appear in this list, each item must be uploaded to the Google Drive Shared Folder</p>
<script type="text/javascript">
var \$table = jQuery('table.datatable-archive');
mm_initDataTable(\$table, $jsontext, 'single-wrapper');
</script>
BLOCK;
}
protected function _getSpeakerDetails($ms_id) {
global $wpdb;
$did = "'" . esc_sql($ms_id) . "'";
$sql = <<<BLOCK
select ms_id,ms_firstname,ms_lastname,ms_suffix,ms_active,ms_ecclesia,mb_name
from mm_speaker
left join mm_branch on ms_ecclesia=mb_id
where ms_id=$did
BLOCK;
return $wpdb->get_row($sql);
}
private function _speaker_detail($ms_id, $data) {
global $wpdb;
$rows = $wpdb->get_results('select mb_id,mb_name from mm_branch order by mb_name', ARRAY_A);
$select = [];
foreach($rows as $row) {
$select[$row['mb_id']] = $row['mb_name'];
}
$fields = $this->FormField($data, 'ms_lastname', 'Last Name') .
$this->FormField($data, 'ms_firstname', 'First Name') .
$this->FormField($data, 'ms_suffix', 'Suffix') .
$this->FormField($data, 'ms_ecclesia', 'Ecclesia', [ 'select' => $select ]) .
$this->FormField($data, 'ms_active', 'Active/Living?', ['checkbox' => TRUE] );
$payload = new \StdClass;
$payload->fields = $fields;
$payload->buttons = $this->submit_button('Save Changes', 'primary', 'submit', FALSE);
$payload->data = $data;
$nonce = wp_nonce_field( 'media-manager:speaker:' . $ms_id, '_wpnonce', FALSE, FALSE);
$url = admin_url( 'admin.php' ) . '?page=speakers&action=' . $ms_id;
echo <<<BLOCK
<h2 class="title">Speaker Details</h2>
<form method="post" action="$url">
<input type="hidden" name="action" value="speaker">
<input type="hidden" name="ms_id" value="{$ms_id}">
$nonce
<table class="form-table speaker-form" role="presentation">
<tbody>{$payload->fields}</tbody>
</table>
<p class="submit">
{$payload->buttons}
</p>
</form>
BLOCK;
}
public function mmc_speaker() {
global $wpdb;
// display list of speakers
$action = $_GET['action'] ?? $_POST['action'] ?? '';
$action = trim($action);
if ($action != '') {
$ms_id = (int) $action;
if ($this->isPost && $ms_id) {
// update the record
$types = [ '%s', '%s', '%s', '%d', '%d' ];
$values = [
'ms_lastname' => trim($_POST['ms_lastname'] ?? ''),
'ms_firstname' => trim($_POST['ms_firstname'] ?? ''),
'ms_suffix' => trim($_POST['ms_suffix'] ?? ''),
'ms_ecclesia' => trim($_POST['ms_ecclesia'] ?? ''),
'ms_active' => intval($_POST['ms_active'] ?? '0', 10),
];
$result = $wpdb->update('mm_speaker', $values, ['ms_id' => $ms_id], $types);
}
$info = $this->_getSpeakerDetails($ms_id);
return $this->_speaker_detail($ms_id, $info);
}
$data = [
'pageLength' => 25,
'dom' => 'lfrtip',
'order' => [[ 1, 'asc' ]],
'autoWidth' => false,
'ajax' => '/~/media-speaker', // _command_datatables_speakerlist
'columns' => [
[ 'data' => 'ms_id', 'width' => 60, 'render' => ['_' => 'disp', 'sort' => 'val'], 'className' => 'textright' ],
[ 'data' => 'ms_lastname', 'width' => 120 ],
[ 'data' => 'ms_firstname', 'width' => 120 ],
[ 'data' => 'ms_suffix', 'width' => 60 ],
[ 'data' => 'mb_name', 'width' => 350],
[ 'data' => 'ms_active', 'width' => 50],
],
];
$jsontext = json_encode($data);
echo <<<BLOCK
<h3>Talks</h3>
<p>Please select a speaker from the list below. New speakers are created when creating a Talk</p>
<div style="width: 800px">
<table class="datatable-single speaker-list" style="margin:0">
<thead>
<tr>
<th>ID</th>
<th>Last Name</th>
<th>First Name</th>
<th>Suffix</th>
<th>Ecclesia</th>
<th>Active?</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<script type="text/javascript">
var \$table = jQuery('table.datatable-single');
mm_initDataTable(\$table, $jsontext, 'single-wrapper');
</script>
BLOCK;
}
public function mmc_single() {
global $wpdb;
$action = empty($_GET['action']) ? '' : trim($_GET['action']);
switch($action) {
case 'class':
$me_id = empty($_GET['eid']) ? 0 : (int) $_GET['eid'];
$mi_id = empty($_GET['cid']) ? '' : trim($_GET['cid']);
$seq = empty($_GET['seq']) ? '' : trim($_GET['seq']);
return $this->action_class($mi_id, $me_id, $seq);
case 'cygnus': // if you mess up, you can re-import without causing any problems
$me_id = empty($_GET['eid']) ? 0 : (int) $_GET['eid'];
return $this->action_import_cygnus($me_id);
case 'cygnus2':
$me_id = empty($_GET['eid']) ? 0 : (int) $_GET['eid'];
$this->action_import_cygnus2($me_id);
echo "Done\n";
return;
case 'googledrive':
$mi_id = empty($_GET['cid']) ? '' : trim($_GET['cid']);
return $this->action_googledrive($mi_id);
case 'whisper':
$mi_id = empty($_GET['cid']) ? '' : trim($_GET['cid']);
return $this->action_whisper($mi_id);
}
$data = [
'pageLength' => 25,
'dom' => 'lfrtip',
'order' => [[ 1, 'desc' ]],
'autoWidth' => false,
'ajax' => '/~/media-talks',
'columns' => [
[ 'data' => 'mi_id', 'className' => 'mi-id'],
[ 'data' => 'mi_date', 'className' => 'mi-date' ],
[ 'data' => 'mi_type', 'className' => 'mi-type' ],
[ 'data' => 'me_topic', 'className' => 'me-topic'],
[ 'data' => 'mi_title', 'className' => 'mi-title'],
[ 'data' => 'mi_speaker', 'className' => 'mi-speaker' ],
[ 'data' => 'mi_flags', 'className' => 'mi-action' ],
],
];
$jsontext = json_encode($data);
$safelink = htmlspecialchars('?page=talks-single&action=cygnus&eid='. static::EXHORT);
echo <<<BLOCK
<h3>Talks</h3>
<p>Please select a talk from the list below. New talks are created from the <a href="?page=talks-events">Event page</a>. Exhortations have their own page; import the latest from <a href="$safelink">cygnus</a>.</p>
<table class="datatable-single">
<thead>
<tr>
<th>Id</th>
<th>Date</th>
<th>Type</th>
<th>Topic</th>
<th>Title</th>
<th>Speaker</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script type="text/javascript">
var \$table = jQuery('table.datatable-single');
mm_initDataTable(\$table, $jsontext, 'single-wrapper');
</script>
BLOCK;
}
// upload a vtt file
public function mmc_vtt() {
$name = 'vtt_file';
$href = '?#';
echo <<<BLOCK
<h3>Upload a VTT file</h3>
<p>This page is used to update the filesystem and the database whenever a captioning file is recreated. The name of the .vtt file to be uploaded must contain the 8-character Item Id.</p>
<div id="vtt_upload_area" style="margin-bottom: 30px">
<div style="width:800px;height:50px;" class="mm-dropzone dropzone-frame multiple" name="{$name}" href="{$href}" oncomplete="vtt_complete">
<div class="dz-message needsclick">
Drop vtt file(s) here or click to upload.
</div>
</div>
</div>
<div class="vtt-status">
</div>
<script type="text/javascript">
function vtt_complete(file, response) {
let \$div = jQuery('.vtt-status').first();
\$div.append(`<div>\${file.name} - \${file.status}</div>`);
};
</script>
BLOCK;
// files are processed at
}
private function _breakName($name) {
$result = new StdClass;
$result->suffix = FALSE;
if (($p = strpos($name, ',')) !== FALSE) {
$result->suffix = strtolower(trim(substr($name, $p + 1), '. '));
$name = substr($name, 0, $p);
}
$p = strpos($name, ' ');
$result->first = trim(substr($name, 0, $p));
$result->last = trim(substr($name, $p + 1));
return $result;
}
private function _dataTables_params() {
$result = new StdClass;
$result->draw = isset($_GET['draw']) ? $_GET['draw'] : 1;
$result->start = isset($_GET['start']) ? (int) $_GET['start'] : 0;
$result->length = isset($_GET['length']) ? (int) $_GET['length'] : 0;
if (!$result->length) $result->length = 25;
$result->search = isset($_GET['search']) ? trim($_GET['search']['value']) : '';
$result->sorting = $this->getSorting($_GET['columns'], $_GET['order']);
return $result;
}
private function _command_datatables_categorylist($dtparam) {
$params = [];
foreach($_GET as $key => $value) {
switch($key) {
case 'speaker':
case 'subject':
case 'location':
case 'myear':
$params[$key] = $value;
}
}
$result = $this->getAudioItems($params, $dtparam);
$result['draw'] = (int) $dtparam->draw;
header('Content-type: application/json');
die(json_encode($result, JSON_UNESCAPED_SLASHES));
}
private function _command_datatables_speakerlist() {
global $wpdb;
$sql = <<<'BLOCK'
select ms_id,ms_firstname,ms_lastname,ms_suffix,ms_active,mb_name
from mm_speaker
left join mm_branch on ms_ecclesia=mb_id
BLOCK;
$rows = $wpdb->get_results($sql, ARRAY_A);
$url = admin_url( 'admin.php' ) . '?page=speakers&action=';
foreach($rows as &$row) {
$row['ms_id'] = [
'disp' => "<a href=\"{$url}{$row['ms_id']}\" target=\"_speaker\">{$row['ms_id']}</a>",
'val' => $row['ms_id'],
];
}
$result = ['data' => $rows];
header('Content-type: application/json');
die(json_encode($result, JSON_UNESCAPED_SLASHES));
}
private function _command_datatables_eventlist() {
global $wpdb;
if ($_SESSION['playlist_valid'] ?? 0) {
$sql = <<<BLOCK
select me_id,me_location,me_slug,me_topic,me_year,me_playlistid,
count(mixe_item) as c
from `mm_event`
right join mm_item_x_event on mixe_event=me_id
left join mm_item on mixe_item=mi_id
where (mi_type="ac" or mi_type="bc")
group by me_id
having c > 1
order by me_year desc,me_id desc
BLOCK;
} else {
$sql = <<<BLOCK
select me_id,me_location,me_slug,me_topic,me_year,me_playlistid,
count(mixe_item) as c
from `mm_event`
right join mm_item_x_event on mixe_event=me_id
group by me_id
order by me_year desc,me_id desc
BLOCK;
}
// and (mi_other like "%youtube.com%" or mi_other like "%youtu.be%")
$rows = $wpdb->get_results($sql, ARRAY_A);
foreach($rows as &$row) {
$j = empty($row['mi_other']) ? [] : json_decode($row['mi_other'], TRUE);
unset($row['mi_other']);
$row['me_topic'] = $this->_normalizeTitle($row['me_topic']);
if (empty($row['me_playlistid'])) $row['me_playlistid'] = '';
}
$result = ['data' => $rows];
header('Content-type: application/json');
die(json_encode($result, JSON_UNESCAPED_SLASHES));
}
// large files to be moved from cdn1 to cdn2
private function _command_datatables_archive() {
$source = 1 << static::ARIES; // found on this
// $nsource = static::ARIES;
$target = 1 << static::GOOGLE; // and found on this
global $wpdb;
// get list of all talks having video on cdn1 but not on cdn3
// sort by size, return top 100
$sql = <<<BLOCK
select mi_id,mi_type,mi_date,mi_title,ms_firstname,ms_lastname,ms_suffix,mi_other,
mm_filesize as _size
from mm_media as m
left join mm_item as i on m.mm_hash = i.mi_id
left join mm_speaker as s on s.ms_id = i.mi_speaker
where (((present_mp4 & $source) <> 0 and (present_mp4 & $target) <> 0 )
or ((present_mp3 & $source) <> 0 and (present_mp3 & $target) <> 0 ))
and mm_source = 1
and (m.mm_type = 'mp4' or m.mm_type = 'mp3')
order by mi_date desc
limit 400
BLOCK;
$rows = $wpdb->get_results($sql);
foreach($rows as &$row) {
$row->mi_speaker = static::AsSpeaker($row);
}
return $rows;
}
// list all classes for an event - in appropriate order
// or list all classes
private function _command_datatables_classeslist($me_id, $allowzero = FALSE, $exhortonly = FALSE) {
global $wpdb, $MEDIASEARCH;
// get all calendar events
$calendar = [];
if (defined('WP_SPIFFYCAL_TABLE')) {
$table = $wpdb->get_blog_prefix().WP_SPIFFYCAL_TABLE;
$date = gmdate('Y-m-d', time() + 2 * 86400);
$sql = "select event_begin,event_category,event_link from $table where event_category<>1 and event_begin <= '$date'";
$rows = $wpdb->get_results($sql);
foreach($rows as $row) {
$category = $row->event_category;
$calendar["{$row->event_begin}:{$row->event_category}"] = $row->event_link;
}
}
$isAdmin = current_user_can('administrator');
$did = "'" . esc_sql($me_id) . "'";
if ($allowzero) {
// $cutoff = "'" . esc_sql(date('Y-m-d', time() - 4 * 86400 * 366)) . "'";
$cutoff = "'" . esc_sql(date('Y-m-d', time() - 20 * 86400 * 366)) . "'";
$where = "mi_date > $cutoff";
$extra = ',mixe_event,me_topic';
} else {
$where = "mixe_event=$did";
$extra = '';
}
if ($exhortonly) {
$where .= ' and mi_type="ex" and x.mixe_event=' . static::EXHORT;
} else {
// $where .= ' and mi_type<>"ex"';
}
// remove cutoff from the sql to list everything
$sql = <<<BLOCK
select mi_id,mi_type,mi_date,mi_title,ms_firstname,ms_lastname,ms_suffix,mi_other,mixe_sequence,
length(mi_thumbnail) as ltn, mg.mm_type as mg_type, mv.mm_type as mv_type,
present_mp4,present_mp3,present_vtt$extra
from mm_item_x_event as x
left join mm_item as i on x.mixe_item = i.mi_id
left join mm_speaker as s on s.ms_id = i.mi_speaker
left join mm_event as e on e.me_id = x.mixe_event
left join mm_media as mg on mg.mm_hash = x.mixe_item and mg.mm_source=5 and (mg.mm_type="mp3" or mg.mm_type="mp4")
left join mm_media as mv on mv.mm_hash = x.mixe_item and mv.mm_source=5 and (mv.mm_type="vtt")
where $where
order by mixe_event, mixe_sequence
BLOCK;
$rows = $wpdb->get_results($sql);
foreach($rows as &$row) {
$row->mi_speaker = static::AsSpeaker($row);
$other = json_decode($row->mi_other);
if (is_object($MEDIASEARCH)) {
$row->media = $MEDIASEARCH->generateAuth($row->mi_type, $row->mi_id, TRUE) . '#' . $row->mi_type . '_' .rawurlencode($row->mi_id);
}
if ($isAdmin) {
$this->source ->MmItem($row);
$row->auth = $this->source->generateAuth('***'); // being deleted
}
$row = (array) $row;
$row['mi_flags'] = [
'vtt' => $row['present_vtt'],
'ltn' => (int) $row['ltn'],
];
$row['_youtube'] = empty($other->youtube) ? '' : $other->youtube;
$temp = ParseVideoUrl($row['_youtube']);
if ($temp->source == 'youtube' && $temp->id !== FALSE) {
$row['_edit'] = "https://studio.youtube.com/video/{$temp->id}/edit";
}
unset($row['present_vtt'], $row['ltn'], $row['mi_other'], $row['ms_firstname'], $row['ms_lastname'], $row['ms_suffix']);
if ($row['mi_date'] > '2022-01-01') { // this is when we started
if ($row['mi_type'] == 'ex') {
$key = "{$row['mi_date']}:2";
} elseif ($row['mi_type'] == 'ac') {
$key = "{$row['mi_date']}:3";
} elseif ($row['mi_type'] == 'bc') {
$key = "{$row['mi_date']}:4";
} else {
$key = '';
}
if ($key != '' && isset($calendar[$key])) {
$incalendar = substr($calendar[$key], -6) == '/video' ? 1 : 2;
$row['calendar'] = $incalendar;
}
}
if (!empty($row['mg_type']) && !empty($row['mg_type'])) {
$row['google'] = 2;
} elseif (empty($row['mg_type']) && empty($row['mg_type'])) {
$row['google'] = 0;
} else {
$row['google'] = 1;
}
$row['mi_date'] = str_replace('-', '.', $row['mi_date']);
}
return $rows;
}
private function _command_datatables_medialist($mi_id) {
$rows = [];
if ($mi_id != '') {
$data = static::Load($mi_id, ['media' => TRUE]);
$media = $data->_media;
$basename = isset($data->_other->original) ? $data->_other->original : '';
if (($p = strrpos($basename, '.')) !== FALSE) {
$basename = substr($basename, 0, $p);
}
// which servers have which files?
$rows = $this->source
->BaseFilename($basename)
->MmItem($data)
->Media($media)
->getListing();
}
header('Content-type: application/json');
$result = ['data' => $rows];
echo json_encode($result, JSON_UNESCAPED_SLASHES);
exit;
}
private function _escape($text) {
global $wpdb;
return '"' . '' . '"';
}
private function _command_autocomplete($q, $surname_first = FALSE) {
global $wpdb;
// $dquery = '"' . $wpdb->remove_placeholder_escape(esc_sql("%$q%")) . '"';
$dquery = esc_sql($q);
$namefield = $surname_first
? 'concat(ms_lastname, " ", ms_firstname)'
: 'concat(ms_firstname, " ", ms_lastname)';
if (strlen($q) == 2 && !$surname_first) {
$fi = esc_sql($q[0]);
$li = esc_sql($q[1]);
$sql = <<<BLOCK
select *,
$namefield as spk
from mm_speaker
where ms_firstname like "{$fi}%" and ms_lastname like "{$li}%"
BLOCK;
} else {
$sql = <<<BLOCK
select *,
$namefield as spk
from mm_speaker
where $namefield like "%{$dquery}%"
BLOCK;
}
if (!$surname_first) $sql .= ' and ms_active=1';
$rows = $wpdb->get_results($sql);
$data = [];
foreach($rows as $row) {
$speaker = $surname_first
? static::AsSpeaker($row)
: trim($row->spk);
$node = [
'id' => $row->ms_id,
'value' => $speaker,
'home' => $row->ms_ecclesia,
];
if ($row->ms_ecclesia == 1) $node['class'] = 'hilite';
$data[] = $node;
}
usort($data, function($a, $b) {
$x = $a['home'] - $b['home'];
if ($x) return $x;
return strcasecmp($a['value'], $b['value']);
});
header('Content-type: application/json');
echo json_encode($data, JSON_UNESCAPED_SLASHES);
exit;
}
private function _command_autocomplete_id($q) {
global $wpdb;
$dquery = '"' . esc_sql("$q%") . '"';
$sql = <<<BLOCK
select mi_id, mi_date, ms_firstname,ms_lastname,ms_suffix
from mm_item
left join mm_speaker on mi_speaker=ms_id
where mi_id like $dquery
BLOCK;
$rows = $wpdb->get_results($sql);
$data = [];
foreach($rows as $row) {
$speaker = static::AsSpeaker($row);
if (!empty($row->mi_date)) $speaker .= " ({$row->mi_date})";
$data[] = [
'id' => $row->mi_id,
'value' => "{$row->mi_id} - $speaker",
];
}
header('Content-type: application/json');
echo json_encode($data, JSON_UNESCAPED_SLASHES);
exit;
}
// if mi_id is specified (and exists), add entry to mm_item_x_event and return to page
// otherwise continue to add-new-talk page
private function _command_new_talk($me_id, $sequence, $mi_id) {
global $wpdb;
if ($mi_id != '') {
$data = static::Load($mi_id);
if (!is_null($data)) { // it's good
$this->addMixie($me_id, $sequence, $mi_id);
header("Location: /wp-admin/admin.php?page=talks-events&action=event&eid={$me_id}");
exit;
}
}
$dseq = rawurlencode($sequence);
header("Location: /wp-admin/admin.php?page=talks-single&action=class&eid={$me_id}&cid=&seq={$dseq}");
exit;
}
private function _form_option($option) {
return esc_attr( get_option( $option ) );
}
private function _e( $text, $domain = 'default' ) {
return translate( $text, $domain );
}
private function _generateEventList($events, $option, $class, $type, $id) {
global $MEDIASEARCH;
$result = '';
foreach($events as $index => $event) {
$value = $event->me_topic . ' - ' . $event->me_location . ' (' . $event->me_year . ')';
$dvalue = esc_html($value);
$external = '#';
if ($event->me_id == static::EXHORT) {
$link2 = '/wp-admin/admin.php?page=talks-exhort';
if (is_object($MEDIASEARCH)) {
$external = $MEDIASEARCH->generateAuth($type, $id, TRUE) . '#' . $type . '_' .rawurlencode($id);
}
} else {
$link2 = '?page=talks-events&action=event&eid=' . $event->me_id;
// this is not right - it should show all items in the specified event
if (is_object($MEDIASEARCH)) {
$external = $MEDIASEARCH->generateAuth($event->me_id, $id, TRUE) . '#' . 'ev' . '_' .rawurlencode($id);
}
}
$result .= <<<BLOCK
<div>
<a href="$link2" id="{$option}_{$index}" class="readonly regular-text ltr $class">$dvalue</a>[{$event->mixe_sequence}] <a
href="$external" target="clientversion"><i class="fas fa-link fa-lg" title="client view"></i></a>
</div>
BLOCK;
}
return $result;
}
public function FormField($data, $option, $caption, $param = []) {
$val = $data->{$option};
$txt = $this->_e($caption);
$class = str_replace('_', '-', $option);
$inputtype = isset($param['type']) ? $param['type'] : '';
if ($inputtype == '') $inputtype = 'text';
$extra = '';
if (isset($param['autocomplete'])) {
$class .= ' mm-autocomplete';
$extra = ' href="' . $param['autocomplete'] . '"';
}
if (isset($param['validate'])) {
$class .= ' mm-validate';
$extra = ' href="' . $param['validate'] . '"';
if (!isset($param['suffix'])) $param['suffix'] = '';
$param['suffix'] .= ' <i class="state1 far fa-check-circle fa-lg" style="color:#090;display:none;"></i><i class="state0 far fa-times-circle fa-lg" style="color:#900;display:none;"></i>';
}
if (isset($param['className'])) {
$class .= ' ' . $param['className'];
}
$dval = is_array($val) ? '' : esc_attr($val);
$field = <<<BLOCK
<input name="$option" type="$inputtype" id="$option" value="$dval" class="regular-text ltr $class"$extra />
BLOCK;
if (!empty($param['checkbox'])) {
$sel = empty($val) ? '' : ' checked';
$suffix = isset($param['suffix']) ? ' ' . $param['suffix'] : '';
return <<<BLOCK
<tr>
<th scope="row"></th>
<td><label><input type="checkbox" name="$option" value="1"$sel> $txt</label>$suffix</td>
</tr>
BLOCK;
} elseif (isset($param['select'])) {
$output = '';
foreach($param['select'] as $key => $value) {
$dkey = esc_attr($key);
$dvalue = esc_html($value);
$sel = $key == $val ? ' selected' : '';
$output .= "<option value=\"$dkey\"$sel>$dvalue</option>";
}
$field = <<<BLOCK
<select name="$option" class="regular-text ltr $class">
$output
</select>
BLOCK;
} else if (!empty($param['events']) && is_array($param['events'])) {
$field = $this->_generateEventList($val, $option, $class, $param['events']['type'], $param['events']['id']);
} else if (!empty($param['readonly'])) {
$dvalue = esc_html($val);
if (is_string($param['readonly'])) {
$durl = esc_html($param['readonly']);
$field = <<<BLOCK
<a href="$durl" id="$option" class="readonly regular-text ltr $class">$dvalue</a>
BLOCK;
} else {
$field = <<<BLOCK
<span id="$option" class="readonly regular-text ltr $class">$dvalue</span>
BLOCK;
}
}
if (isset($param['suffix'])) {
$field .= $param['suffix'];
}
return <<<BLOCK
<tr>
<th scope="row"><label for="$option">$txt</label></th>
<td>$field</td>
</tr>
BLOCK;
}
function submit_button( $text = null, $type = 'primary', $name = 'submit', $wrap = true, $other_attributes = null ) {
return get_submit_button( $text, $type, $name, $wrap, $other_attributes );
}
private function action_event($me_id) {
global $wpdb;
if ($me_id) {
$did = "'" . esc_sql($me_id) . "'";
$data = $wpdb->get_row("select * from mm_event where me_id=$did");
$me_id = (int) $me_id;
} else {
$data = NULL;
}
if (is_null($data)) {
$data = new StdClass;
$data->me_topic = '';
$data->me_slug = '';
$data->me_location = 'Stoughton';
$data->me_year = date('Y');
$data->me_aslisting = 0;
$data->me_showlink = 1;
$data->__existing = FALSE;
} else {
$data->__existing = TRUE;
}
if ($data->me_slug != '') {
$suffix = '<a href="/' . $data->me_slug . '" target="extra" style="margin-left:12px"><i class="fas fa-link fa-lg"></i></a>';
} else {
$suffix = '';
}
$sql = "select max(mixe_sequence) as mx from `mm_item_x_event` where mixe_event={$me_id}";
$temp = $wpdb->get_row($sql);
$next_sequence = $temp->mx + 1; // not for exhorts, which should be based on todays date
// wp-admin/options-writing
$fields = $this->FormField($data, 'me_topic', 'Topic') .
$this->FormField($data, 'me_slug', 'Slug', ['suffix' => $suffix, 'validate' => '/~/media-validate/' . $me_id]) .
$this->FormField($data, 'me_location', 'Location') . // combobox allowing paste of existing locations
$this->FormField($data, 'me_year', 'Year') . // default to current year on new record
$this->FormField($data, 'me_aslisting','Display as a Listing', ['checkbox' => TRUE] ) .
$this->FormField($data, 'me_showlink', 'Show Original URL', ['checkbox' => TRUE] );
$payload = new \StdClass;
$payload->fields = $fields;
$payload->buttons = $this->submit_button('Save Changes', 'primary', 'submit', FALSE);
$payload->data = $data;
do_action('event_fields', $payload, $this);
$nonce = wp_nonce_field( 'media-manager:event:' . $me_id, '_wpnonce', FALSE, FALSE);
echo <<<BLOCK
<h2 class="title">Event Details</h2>
<form method="post" action="options-general.php?page=talks-events&action=event">
<input type="hidden" name="action" value="media_manager_class_event">
<input type="hidden" name="me_id" value="{$me_id}">
$nonce
<table class="form-table media-event-form" role="presentation">
<tbody>{$payload->fields}</tbody>
</table>
<p class="submit">
{$payload->buttons}
</p>
</form>
BLOCK;
if ($data->__existing) {
$data = [
'x_me_id' => $me_id,
'x_admin' => current_user_can('administrator') ? 1 : 0,
'x_sequence' => $next_sequence,
'pageLength' => 25,
'dom' => 'lfrtipB<"newstuff">',
'autoWidth' => false,
'ajax' => '/~/media-classes/' . $me_id,
'columns' => [
// [ 'data' => 'mi_thumb', 'className' => 'me-topic'],
[ 'data' => 'mixe_sequence', 'className' => 'mi-sequence'],
[ 'data' => 'mi_date', 'className' => 'mi-date' ],
[ 'data' => 'mi_title', 'className' => 'mi-title'],
[ 'data' => 'mi_speaker', 'className' => 'mi-speaker' ],
[ 'data' => '_youtube', 'className' => 'mi-title'],
[ 'data' => 'mi_flags', 'className' => 'mi-action', 'width' => '210px' ],
],
'buttons' => [
[
//'src' => '?page=talks-single&action=class&eid='. $me_id . '&cid=',
'href' => '#',
'text' => '+',
'hint' => 'Create new talk...',
'tag' => 'a',
'className' => 'newtalk',
], [
'href' => '/~/media-archive/' . $me_id,
'text' => '<i class="fa fa-archive" aria-hidden="true"></i>',
'hint' => 'archive all',
'tag' => 'a',
'className' => 'confirm-archive',
]
],
];
$jsontext = json_encode($data);
// actions - edit, hasCC, link to watch/listen page
// <th>Thumb?</th>
echo <<<BLOCK
<table class="datatable-classes">
<thead>
<tr>
<th>#</th>
<th>Date</th>
<th>Title</th>
<th>Speaker</th>
<th>Original Url</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script type="text/javascript">
var \$table = jQuery('table.datatable-classes');
mm_initDataTable(\$table, $jsontext, 'classes-wrapper');
</script>
BLOCK;
}
}
static
private function AsSpeaker($item) {
// $value = trim($item->ms_lastname . ', ' . $item->ms_firstname);
$value = trim($item->ms_firstname . ' ' . $item->ms_lastname);
if (!empty($item->ms_suffix)) $value .= ', ' . $item->ms_suffix;
return $value == ',' ? '-' : $value;
}
static
public function Load($mi_id, $options = []) {
global $wpdb;
$include_media = array_key_exists('media', $options) ? $options['media'] : FALSE;
$first = array_key_exists('first', $options) ? $options['first'] : TRUE;
$thumb = array_key_exists('thumb', $options) ? $options['thumb'] : $first; // thumbnail wanted?
$order = array_key_exists('order', $options) ? $options['order'] : '';
$group = array_key_exists('group', $options) ? $options['group'] : '';
$limit = array_key_exists('limit', $options) ? (int) $options['limit'] : 0;
$debug = array_key_exists('debug', $options) ? (int) $options['debug'] : 0;
if (is_string($mi_id) || is_numeric($mi_id)) {
$where = 'mixe_item="' . esc_sql($mi_id) . '"';
} elseif (is_array($mi_id)) {
$where = join(' and ', $mi_id);
} else {
die('Unable to WHERE ' . var_export($mi_id, 1));
}
$thumb = $thumb
? '*'
: 'mixe_event,mixe_sequence,mixe_item,'.
'mi_id,mi_type,mi_date,mi_speaker,mi_title,mi_other,present_mp3,present_mp4,present_vtt,present_jpg,present_xml,'.
'ms_firstname,ms_lastname,ms_suffix,'.
'me_year,me_location,me_topic,me_slug,me_aslisting,me_showlink';
$sql = <<<BLOCK
select $thumb,
length(mi_thumbnail) as ltn,
max(mm_duration) as `duration`
from mm_item_x_event as x
inner join mm_item as i on i.mi_id = x.mixe_item
left join mm_speaker as s on s.ms_id = i.mi_speaker
left join mm_event as e on e.me_id = x.mixe_event
left join mm_media as m on m.mm_hash = x.mixe_item and (m.mm_type="mp3" or m.mm_type="mp4")
where $where
BLOCK;
// max(mm_duration) as duration
if ($group != '') $group = ", $group";
if (is_string($mi_id)) {
$sql .= ' group by x.mixe_event, m.mm_hash '. $group;
} else {
$sql .= ' group by m.mm_hash '. $group;
}
if ($order != '') $sql .= ' order by '. $order;
if ($first) {
$sql .= ' limit 3'; // talk may be in upto 3 events
} elseif ($limit) {
$sql .= ' limit ' . $limit;
}
if ($debug) {
echo $sql . "\n";
}
$rows = $wpdb->get_results($sql);
if (is_null($rows) || !count($rows)) {
return NULL;
}
$ids = [];
foreach($rows as $index => $row) {
$event = new StdClass;
$event->me_id = $row->me_id ?? 0;
$event->me_year = $row->me_year;
$event->me_location = $row->me_location;
$event->me_topic = $row->me_topic;
$event->me_slug = $row->me_slug;
$event->me_aslisting = $row->me_aslisting;
$event->me_showlink = $row->me_showlink;
$event->mixe_sequence = $row->mixe_sequence;
if (isset($ids[$row->mi_id])) { // this talk is part of another event
unset($rows[$index]);
$row = $rows[$ids[$row->mi_id]];
$row->_event[] = $event;
} else {
$ids[$row->mi_id] = $index;
if (empty($row->mi_other)) {
$row->_other = new stdClass;
} else {
$row->_other = json_decode($row->mi_other);
}
$row->_speaker = static::AsSpeaker($row);
$row->_media = [];
$row->_event = [$event];
}
}
if ($include_media) {
$list = array_map(function($x) {
return '"' . esc_sql($x) . '"';
}, array_keys($ids));
$list = join(',', $list);
$medias = $wpdb->get_results("select * from mm_media where mm_hash in ($list) order by mm_hash,mm_type,mm_source");
foreach($medias as $item) {
$hash = $item->mm_hash;
$item->mm_other = json_decode($item->mm_other);
$index = $rows[$ids[$hash]]->_media[] = $item;
}
}
return ($first) ? $rows[0] : $rows;
}
private function action_class_update($mi_id, $sig) {
global $wpdb;
header('Content-type: text/plain');
$data = static::Load($mi_id);
$tar = $data->mi_id . '.mp4';
$expected_sig = substr(md5('its-ok:' . $tar), 12, 8);
if ($expected_sig == $sig && !is_null($data)) {
$data->present_mp4 |= 1 << static::HOSTZ;
if ($data->mi_type == 'ac') {
$data->_other->original = str_replace('-', '.', $data->mi_date) . ' ac.mp4';
}
$did = '"' . esc_sql($data->mi_id) . '"';
$safeother = '"' . esc_sql(json_encode($data->_other, JSON_UNESCAPED_SLASHES)) . '"';
$wpdb->Query("update mm_item set present_mp4={$data->present_mp4},mi_other={$safeother} where mi_id=$did");
// redirect back again
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $mi_id);
exit;
}
die('Unauthorized');
}
private function action_class($mi_id, $me_id, $seq) {
global $wpdb, $MEDIASEARCH;
$data = static::Load($mi_id);
if (is_null($data)) { // Create a default record
$devent = '"' . esc_sql($me_id) . '"';
$sql = <<<BLOCK
select e.*,
max(i.mi_type) as mi_type,
max(i.mi_speaker) as mi_speaker
from mm_event as e
left join mm_item_x_event as x on x.mixe_event = e.me_id
left join mm_item as i on x.mixe_item = i.mi_id
where e.me_id=$devent
BLOCK;
$data = $wpdb->get_row($sql);
if (is_null($data)) {
$data = new StdClass;
$data->_event = [];
} else {
$event = new StdClass;
$event->me_id = $me_id;
$event->me_year = $data->me_year;
$event->me_location = $data->me_location;
$event->me_topic = $data->me_topic;
$event->me_slug = $data->me_slug;
$event->mixe_sequence = $seq;
$data->_event = [$event];
}
$data->mi_id = FALSE;
$data->mi_title = '';
$data->mi_date = date('Y-m-d');
$data->_speaker = '';
// default type depends on last item in this event
if ($data->mi_type != 'ex' && $data->mi_type != 'bc' && $data->mi_type != '') {
$def = (int) $data->mi_speaker;
if ($def) { // select default speaker
$spkr = $wpdb->get_row("select * from mm_speaker where ms_id = $def");
$data->_speaker = static::AsSpeaker($spkr);
}
} else {
$data->_speaker = '';
}
$data->_other = [];
$data->__existing = FALSE;
} else {
$data->_speaker = static::AsSpeaker($data);
$data->__existing = TRUE;
$data->_other = json_decode($data->mi_other);
}
$data->_youtube = empty($data->_other->youtube) ? '' : $data->_other->youtube;
if (empty($data->mi_id)) {
$data->_id = '(new)';
$link = TRUE;
} else {
$data->_id = $data->mi_id;
if (is_object($MEDIASEARCH)) {
$link = $MEDIASEARCH->generateAuth($data->mi_type, $data->mi_id, TRUE) . '#' . $data->mi_type . '_' .rawurlencode($data->mi_id);
} else {
$link = TRUE;
}
}
$suffix = $this->hasOldVersion($data); // old version present?
if ($data->_speaker == '-' || $data->_speaker == '-,' ) $data->_speaker = '';
$youtubies = '';
if (!empty($data->_youtube)) {
$safetext = htmlspecialchars($data->_youtube);
$res = ParseVideoUrl($data->_youtube);
// youtube-dl does not format this correctly
//<a href="/~/media-youtube-cc/{$data->mi_id}" style="margin-left:15px"><i class="far fa-closed-captioning fa-lg" title="download youtube automatic closed captions"></i></a>
$youtubies = <<<BLOCK
<a href="{$safetext}" target="_blank" style="margin-left:15px"><i class="fas fa-external-link-alt fa-lg" title="open external link in a new tab"></i></a>
BLOCK;
// only for OTHER types (one source)
if ($res->source == 'youtube' && $data->mi_type == 'other') {
$youtube = new Youtube_Model;
$youtube->Url($data->_youtube);
$present = $youtube->CaptionPresent();
if ($present) {
$youtubies .= <<<BLOCK
<a href="/~/media-captions/{$data->mi_id}" style="margin-left:15px"><i class="far fa-closed-captioning fa-lg" title="import youtube captions"></i></a>
BLOCK;
}
if ($youtube->title && empty($data->mi_title)) {
$youtubies .= <<<BLOCK
<a href="/~/media-title/{$data->mi_id}" style="margin-left:15px"><i class="fas fa-quote-left fa-lg" title="import youtube title"></i></a>
BLOCK;
}
if (count($youtube->poster)) {
$youtubies .= <<<BLOCK
<a href="/~/media-thumbnail/{$data->mi_id}" style="margin-left:15px"><i class="far fa-file-image fa-lg" title="import youtube thumbnail"></i></a>
BLOCK;
}
$youtubies .= <<<BLOCK
<a href="/~/media-import/{$data->mi_id}" style="margin-left:15px"><i class="fas fa-file-import fa-lg" title="import youtube video"></i></a>
BLOCK;
}
}
if ($data->mi_type == 'cbt') {
$youtubies .= " <a href=\"https://cygnus.hopeinstoughton.org/videoimport/chbibletalks?ident={$data->mi_moorestown}\">Import</a>";
}
$primary_event = $data->_event[0];
if (count($data->_event) > 1) {
foreach($data->_event as $e) {
if ($e->me_id != static::EXHORT) {
$primary_event = $e;
break;
}
}
}
if ($primary_event->me_aslisting && is_object($MEDIASEARCH)) {
$link = $MEDIASEARCH->generateAuth('in', $data->mi_id, TRUE);
}
$fields = $this->FormField($data, '_id', 'Identifier', ['readonly' => $link ] ).
$this->FormField($data, '_event', 'Event(s)', ['events' => ['type' => $data->mi_type, 'id' => $data->mi_id ] ] ) .
$this->FormField($data, 'mi_type', 'Type', ['select' => self::TYPES] ) .
$this->FormField($data, '_speaker', 'Speaker', ['autocomplete' => '/~/media-autocomplete'] ) .
$this->FormField($data, 'mi_title', 'Title') .
$this->FormField($data, 'mi_date', 'Date', ['type' => 'date', 'suffix' => $suffix] ) . // put in calendar picker
$this->FormField($data, '_youtube', 'Original URL', ['suffix' => $youtubies] ) ; // to do: put in link icon
// mp3 - show current tags/thumbnail
// - audio uploaded to church
// add thumbnail to mp3?
// determine filesize and duration
$dmi_id = esc_attr($mi_id);
$button = $this->submit_button();
$thumb = '';
if (!empty($data->mi_thumbnail)) {
$thumb = $this->_scalethumb($data->mi_thumbnail);
$thumb = "<div class=\"mm-thumbnail\">{$thumb}</div>";
}
// $me_id & $seq only used when creating a new event:
$dseq = esc_attr($seq);
echo <<<BLOCK
<h2 class="title">Talk Details</h2>
<form method="post" action="admin.php?page=talks-single&action=class" style="position:relative;max-width:1200px">
$thumb
<input type="hidden" name="action" value="media_manager_class_classes">
<input type="hidden" name="seq" value="{$dseq}">
<input type="hidden" name="me_id" value="{$me_id}">
<input type="hidden" name="mi_id" value="{$dmi_id}">
<table class="form-table media-class-form" role="presentation">
<tbody>$fields</tbody>
</table>
$button
</form>
BLOCK;
// table of files associated with this item,
// server, filetype, filesize, download links
// drag-and-drop xml - vtt - mp4 - mp3 - nomark
// max filesize?
// show the datatable - of files and sources - drag and drop
if ($data->__existing) {
$name = trim($data->ms_firstname . ' ' . $data->ms_lastname);
if ($data->ms_suffix) $name .= ' '. ucfirst($data->ms_suffix);
$payload = [
'pageLength' => 25,
'dom' => 'lfrtipB',
'autoWidth' => false,
'ajax' => '/~/media-items/' . $mi_id,
'columns' => [
[ 'data' => 'i_type', 'className' => 'i-type'],
[ 'data' => 'i_filename', 'className' => 'i-filename' ],
[ 'data' => 'i_duration', 'className' => 'i-duration'],
[ 'data' => 'i_dimension', 'className' => 'i-dimension'],
[ 'data' => 'i_filesize', 'className' => 'i-filesize'],
[ 'data' => 'i_location', 'className' => 'i-location'],
[ 'data' => 'i_flags', 'className' => 'i-action' ],
],
'buttons' => [
[
'href' => '?page=talks-single&action=whisper&cid=' . $mi_id,
'text' => '<i class="far fa-closed-captioning fa-lg"></i>',
'hint' => 'Add to Whisper Queue',
'tag' => 'a',
], [
'href' => '?page=talks-single&action=googledrive&cid=' . $mi_id,
'text' => '<i class="fab fa-google-drive fa-lg"></i>',
'hint' => 'Export to Google Drive',
'tag' => 'a',
]
],
'x_mi_id' => $mi_id,
'x_mi_type' => $data->mi_type,
'x_present_vtt' => $data->present_vtt,
'x_defaults' => [
'album' => $primary_event->me_topic,
'title' => $data->mi_title,
'artist' => $name,
'year' => $primary_event->me_year,
'track' => $primary_event->mixe_sequence,
],
];
$jsontext = json_encode($payload);
$auth = '';
$this->source ->MmItem($data);
$auth = $this->source->generateAuth('***'); // being deleted
echo <<<BLOCK
<table class="datatable-media">
<thead>
<tr>
<th>Type</th>
<th>Filename</th>
<th>Duration</th>
<th>Dimensions</th>
<th>Filesize</th>
<th>Location</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<br>
<div class="mm-plupload reload" href="/~/media-fileupload/{$mi_id}" style="max-width: 1200px">
<p>Your browser doesn't support HTML5.</p>
</div>
<p>Upload .mp4, .mp3 and .vtt/.srt files, or .png/.jpg thumbnail for this talk. Attachments should be zipped and uploaded as a single .zip. Use .html for Inline html.</p>
<script type="text/javascript">
var \$table = jQuery('table.datatable-media');
mm_initDataTable(\$table, $jsontext, 'media-wrapper');
</script>
<p><a href="/~/media-trash/{$auth}" onclick="javascript:return confirm('Are you sure you wish to completely zap this talk?') == 1;">Completely remove this talk</a></p>
BLOCK;
// show free space on each server
}
}
// this is a very low quality image
private function _scalethumb($text) {
$text = preg_replace_callback('~(width|height)="(.*?)"~', function($m) {
$s = 2.4 * (int) $m[2];
if ($m[1] == 'width') {
return "{$m[1]}=\"$s\"";
}
return '';
}, $text);
return $text;
}
// do we have the old version around - allow it to be copied instead of uploading
private function hasOldVersion($data) {
global $wpdb;
$mi_id = $data->mi_id;
$type = $data->mi_type;
$slug = $data->me_slug;
$date = $data->mi_date;
if ($mi_id === FALSE) {
$data = NULL;
} else {
$data = static::Load($mi_id);
}
if ($type == 'ac' && $slug != '') {
// find it in the old system
$xdate = '"' . esc_sql($date) . '"';
$xslug = '"' . esc_sql($slug) . '"';
$sql = <<<BLOCK
select *
from `cd_seriest` as t
inner join `cd_seriesh` as h on cst_series = csh_id
where cst_date=$xdate and csh_slug=$xslug and csh_type="$type"
BLOCK;
$row = $wpdb->get_row($sql);
if (is_null($row)) return NULL; // not recognized
// does the video file exist?
$basename = str_replace('-', '.', $date) . ' ac';
if (file_exists("/mnt/media/current/{$basename}.mp4")) {
$this->source ->MmItem($data);
$link = $this->source->generateDownloadLink(static::HOSTZ, '.mp4', 'info');
$dname = rawurlencode($basename);
return '<a href="' . $link . '/import?name=' . $dname . '&type=ac" style="margin-left:20px" title="import this item from the old system"><i class="fas fa-file-import fa-lg"></i></a>';
}
}
if ($type == 'ex' && !empty($data->_other->original)) {
$filename = $data->_other->original;
if (file_exists('/mnt/media/exhort/video/' . $filename)) { // does the file exist?
$this->source ->MmItem($data);
$link = $this->source->generateDownloadLink(static::HOSTZ, '.mp4', 'info');
$dname = rawurlencode(str_replace('.mp4', '', $filename));
return '<a href="' . $link . '/import?name=' . $dname . '&type=ex" style="margin-left:20px" title="import this item from the old system"><i class="fas fa-file-import fa-lg"></i></a>';
}
}
return NULL;
}
private function getAudioItems($params, $dtparam) {
global $wpdb, $MEDIASEARCH;
$where = ['(present_mp3<>0 or present_mp4<>0)', '(mi_type="mt" or mi_type="cbt")'];
if (isset($params['speaker'])) {
$where[] = "mi_speaker='" . esc_sql($params['speaker']) . "'";
}
if (isset($params['subject'])) {
$where[] = "me_topic='" . esc_sql($params['subject']) . "'";
}
if (isset($params['location'])) {
$where[] = "me_location='" . esc_sql($params['location']) . "'";
}
if (isset($params['myear'])) {
$where[] = "me_year='" . esc_sql($params['myear']) . "'";
}
// get total records, total filtered records
$w = join(' and ', $where);
$sql = <<<BLOCK
select count(x.mixe_item) as ttl
from mm_item_x_event as x
inner join mm_item as i on i.mi_id=x.mixe_item
inner join mm_event as e on e.me_id = x.mixe_event
left join mm_speaker as s on s.ms_id = i.mi_speaker
where $w
BLOCK;
$total = $wpdb->get_row( $sql );
$total->filtered = $total->ttl;
// filtered records
if ($dtparam->search != '') {
$where[] = $this->generateSearch($dtparam->search);
$w = join(' and ', $where);
$sql = <<<BLOCK
select count(x.mixe_item) as ttl
from mm_item_x_event as x
inner join mm_item as i on i.mi_id=x.mixe_item
inner join mm_event as e on e.me_id = x.mixe_event
left join mm_speaker as s on s.ms_id = i.mi_speaker
where $w
BLOCK;
$filtered = $wpdb->get_row( $sql );
$total->filtered = $filtered->ttl;
}
// get records just in specified range, after applying sort order
$orderby = $this->generateSorting($dtparam->sorting);
// $where also includes text filter
$w = join(' and ', $where);
$sql = <<<BLOCK
select mi_id,mi_title,ms_firstname,ms_lastname,ms_suffix,mi_other,
me_year,me_location,me_topic,present_mp3,present_mp4,
mixe_sequence,
max(mm_duration) as `mm_duration`
from mm_item_x_event as x
inner join mm_item as i on i.mi_id=x.mixe_item
inner join mm_event as e on e.me_id = x.mixe_event
left join mm_speaker as s on s.ms_id = i.mi_speaker
left join mm_media as m on mm_hash=mi_id and mm_type="mp3"
where $w
group by mixe_item
$orderby
limit {$dtparam->start}, {$dtparam->length}
BLOCK;
$items = $wpdb->get_results( $sql );
$data = [];
foreach($items as $item) {
$other = json_decode($item->mi_other);
$duration = static::asHMS($item->mm_duration);
$speaker = static::AsSpeaker($item);
$node = [
'col1' => [ $item->mi_title, $item->me_location ],
'col2' => [ $speaker, $item->me_year ],
'col3' => [ $item->me_topic, $item->mixe_sequence ],
'col4' => [ $item->mi_id, $other->original, 0, $duration], // size & duration
];
if (is_object($MEDIASEARCH)) {
$cdn = apply_filters('mm_generate_link', '', $item->mi_id, '.mp3', $item->present_mp3);
$type = $item->mi_type ?? 'o';
$node['media'] = $MEDIASEARCH->generateAuth($type, $item->mi_id, TRUE) . '#' . $type . '_' .rawurlencode($item->mi_id);
if (!empty($cdn)) $node['cdn'] = $cdn;
}
$data[] = $node;
}
return [
'recordsTotal' => (int) $total->ttl,
'recordsFiltered' => (int) $total->filtered,
'data' => $data,
];
}
static
private function asHMS($duration) {
if ($duration > 3599) {
$hours = floor($duration / 3600);
$duration %= 3600;
$duration = sprintf('%d:%02d:%02d', $hours, floor($duration / 60), $duration % 60);
} else {
$duration = sprintf('%02d:%02d', floor($duration / 60), $duration % 60);
}
return $duration;
}
private function getSorting($columns, $order) {
$cols = [];
foreach($columns as $idx => $col) {
$cols[$idx] = $col['data'];
}
foreach($order as &$item) {
$item['column'] = $cols[$item['column']];
}
return $order;
}
private function generateSorting($sorting) {
$result = '';
foreach($sorting as $entry) {
$direction = ($entry['dir'] == 'desc') ? 'desc' : 'asc';
switch($entry['column']) {
case 'col1':
if ($result != '') $result .= ',';
$result .= "mi_title $direction";
break;
case 'col2':
if ($result != '') $result .= ',';
$result .= "me_year $direction, ms_lastname $direction, ms_firstname $direction, me_topic $direction, mixe_sequence";
break;
case 'col3':
if ($result != '') $result .= ',';
$result .= "me_topic $direction, mixe_sequence $direction";
break;
}
break;
}
return ($result != '') ? "order by $result" : '';
}
private function generateSearch($search) {
$safe_search = "'%" . esc_sql($search) . "%'";
$output = '';
foreach(['mi_title', 'ms_lastname', 'ms_firstname', 'me_topic', 'me_year', 'me_location'] as $field) {
if ($output != '') $output .= ',';
$output .= "ifnull(`$field`, ''), ' '";
}
return "concat($output) like $safe_search";
}
// grab some files that are not present on the target, and copy them
private function copyfiles($target) {
$k = 1 << $target;
set_time_limit(0);
global $wpdb;
$sql = <<<BLOCK
select mi_id,mi_type,mi_other,present_mp3,present_vtt
from `mm_item`
where mi_type="mt"
and present_mp3 <> 0
and (present_mp3 & $k = 0)
limit 32
BLOCK;
echo "<h3>Copying to Server #$target</h3>";
$rows = $wpdb->get_results($sql);
foreach($rows as $data) {
$data->_other = json_decode($data->mi_other);
$basename = isset($data->_other->original) ? $data->_other->original : '';
if (($p = strrpos($basename, '.')) !== FALSE) {
$basename = substr($basename, 0, $p);
}
$dt = date('Y-m-d H:i:s');
echo "$dt: Copying {$data->mi_id} $basename<br>\n";
$ok1 = $this->source->CopyFile($data->mi_id . '.mp3', $data->present_mp3, $target, $data->mi_type); // mi_type is deprecated
$ok2 = $this->source->CopyFile($data->mi_id . '.vtt', $data->present_vtt, $target, $data->mi_type);
// update `present` fields
$did = '"'. esc_sql($data->mi_id) . '"';
$ok1 = $ok1 ? $k : 0;
$ok2 = $ok2 ? $k : 0;
$wpdb->Query("update mm_item set present_mp3 = present_mp3 | $ok1, present_vtt = present_vtt | $ok2 where mi_id=$did");
flush();
}
}
private function _getExternalMedia($external_url) {
$res = ParseVideoUrl($external_url);
if ($res->source == 'youtube' && $res->id !== FALSE) {
$result = new Youtube_Model;
return $result->Url("https://youtu.be/" . $res->id);
}
return FALSE; // unrecognized url
}
private function _getExternalMediaURL($mi_id) {
$data = static::Load($mi_id);
if (is_null($data)) return NULL; // item not found
if (empty($data->_other->youtube)) return FALSE; // Original Url has not been specified
return $data->_other->youtube;
}
// this has been updated to use yt-dlp
private function _command_download_youtube_caption($mi_id) {
$url = $this->_getExternalMediaURL($mi_id);
header('Content-type: text/plain');
if (is_null($url)) {
echo "unknown talk identifier";
exit;
}
$youtube = $this->_getExternalMedia($url);
if ($youtube === FALSE) {
echo "Original Url is missing or unrecognized";
exit;
}
$filename = $youtube->DownloadVTT();
if (!file_exists($filename)) {
die("Unable to download captions:n\n{$youtube->debug_output}\n\n");
}
$temp = new \StdClass;
$temp->content = file_get_contents($filename);
$result = $this->_youtube_saveVtt($temp, $mi_id); // save the captions
unlink($filename);
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $mi_id . '&info=' . rawurlencode($result));
exit;
}
private function _command_download_youtube_video($mi_id) {
$url = $this->_getExternalMediaURL($mi_id);
header('Content-type: text/plain');
if (is_null($url)) {
echo "unknown talk identifier";
exit;
}
$youtube = $this->_getExternalMedia($url);
if ($youtube === FALSE) {
echo "Original Url is missing or unrecognized";
exit;
}
$filename = $youtube->DownloadVideo();
if (!file_exists($filename)) {
die("Unable to download video \"$filename\":n\n{$youtube->debug_output}\n\n");
} elseif (!filesize($filename)) {
die("No video downloaded \"$filename\":n\n{$youtube->debug_output}\n\n");
}
$originalname = $mi_id . '.mp4';
$file = [];
$file['tmp_name'] = $filename;
$file['name'] = wp_unslash($originalname);
$file['size'] = filesize($filename);
$res = $this->_uploadComplete($mi_id, $file);
unlink($file['tmp_name']);
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $mi_id );
exit;
}
private function _command_download_youtube_title($mi_id) {
global $wpdb;
$url = $this->_getExternalMediaURL($mi_id);
$youtube = $this->_getExternalMedia($url);
if (is_object($youtube)) {
$did = '"' . esc_sql($mi_id) . '"';
$dtitle = '"' . esc_sql($youtube->title) . '"';
$wpdb->Query("update mm_item set mi_title = $dtitle where mi_id=$did");
}
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $mi_id);
exit;
}
private function _command_download_youtube_thumbnail($mi_id) {
$data = static::Load($mi_id);
$url = $data->_other->youtube ?? FALSE;
$youtube = $this->_getExternalMedia($url);
if (is_object($youtube)) {
list($ht, $imagedata) = $youtube->GetThumbnail(-1); // get poster
list($ht2, $imagedata2) = $youtube->GetThumbnail(110); // thumb: smallest thumbnail that is at least this height
$imagedata2 = $ht2 != 110 ? NULL : $this->_ImageTothumbnail($imagedata2); // no suitable image - create it from the thumb
$this->savePoster($data, $imagedata, $imagedata2);
}
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $mi_id);
exit;
}
private function _youtube_getCaptions($youtube_url) {
$result = new StdClass;
$result->error = FALSE;
$youtube = $this->_getExternalMedia($youtube_url);
if ($youtube === FALSE) {
$result->error = 'Unrecognized URL';
} else {
$result->content = $youtube->Caption();
if ($result->content == '') {
$result->error = 'Captions are not yet available';
}
}
return $result;
}
private function retrieveYoutube($youtubies) {
$youtube = $this->_youtube_getCaptions($youtubies);
if ($youtube->error === FALSE) {
_retrieveYoutube_processFile($youtube);
} else {
$result = $youtube->error;
}
return '/wp-admin/admin.php?page=talks-events&info=' . rawurlencode($result);
}
private function _youtube_saveVtt($result, $mi_id) {
global $wpdb;
$data = static::Load($mi_id);
if (!is_null($data)) {
$filename = $this->source
->MmItem($data)
->getDestination(static::ARIES, "{$mi_id}.vtt");
file_put_contents($filename, $result->content); // add to alcor cdn
// update database
$ok2 = 1 << static::ARIES;
$did = '"' . esc_sql($mi_id) . '"';
$wpdb->Query("update mm_item set present_vtt = present_vtt | $ok2 where mi_id=$did");
$this->UpdateMediaTable($mi_id, static::ARIES, 'vtt');
return "SUCCESS - Item '$mi_id' was updated";
} else {
return "Item '$mi_id' not found in database ($safeid)";
}
}
private function _retrieveYoutube_processFile($youtube) {
global $wpdb;
$content = $youtube->content;
$title = $youtube->title;
// work out what entry it is for
if (preg_match('~^(\d{4})\s(\d{2})\s(\d{2})\sac~i', $title, $match)) {
$date = "{$match[1]}-{$match[2]}-{$match[3]}";
$ddate = '"' . esc_sql($date) . '"';
$dtype = '"' . esc_sql('ac') . '"';
$row = $wpdb->get_row("select mi_id from mm_item where mi_date=$ddate and mi_type=$dtype");
if (is_null($row)) {
return "Event '$date-ac' was not found in the database";
}
return $this->_youtube_saveVtt($result, $row->mi_id);
} else if (preg_match('~^([0-9a-f]{1,10})$~i', $title)) { // hex string
return $this->_youtube_saveVtt($result, $title);
} else {
return "unable to locate suitable entry for '$title' ($safeid)";
}
// a background task will make sure all sites have the files
// and that each has enough free space
}
protected function AsTime($time) {
$seconds = (int) $time;
$millisec = intval(($time - $seconds) * 1000);
while (strlen($millisec) < 3) $millisec = '0' . $millisec;
$ss = intval($seconds % 60);
$seconds = intval($seconds / 60);
$mm = intval($seconds % 60);
$hh = intval($seconds / 60);
return sprintf('%02d:%02d:%02d', $hh, $mm, $ss) . '.' . $millisec ;
}
protected function AsVTT($words, $nexttime) { // nexttime is the start time of the next phrase
$output = '';
while (count($words)) {
$line = '';
$last = 0;
// extract a phrase
foreach($words as $index => $word) {
if (strlen($line) + strlen($word['word']) < 55) {
$last = $index;
$line .= $word['word'];
}
}
if ($last == count($words) - 2) $last = count($words) - 1; // only one word remains - include it
$phrase = array_splice($words, 0, $last + 1);
$words = array_values($words);
$next = isset($words[0]) ? $words[0]['start'] : $nexttime;
$output .= $this->_asvtt_processPhrase($phrase, $next);
// drop the phase and repeat
// process the phrase
}
return $output;
}
protected function _asvtt_processPhrase($phrase, $nexttime) {
$c = count($phrase) - 1;
$start = $phrase[0]['start'];
$end = $phrase[$c]['end'];
if ($end + 0.5 < $nexttime) $end += 0.5; // keep visible for a fraction of a second
// $text = trim(join('', array_column($phrase, 'word')));
$text = '';
foreach($phrase as $index => $entry) {
if (!$index) {
$text .= trim($entry['word']);
} else {
$text .= '<' . $this->AsTime($entry['start']) . '><c>' . $entry['word'] . '</c>';
}
}
$output = $this->AsTime($start) . ' --> ' . $this->AsTime($end) . "\n";
$output .= $text . "\n";
$output .= "\n";
return $output;
}
private function _uploadComplete($mi_id, $file) {
global $wpdb;
$payload = [
'result' => 'err',
'message' => 'unknown file extension',
'reload' => 0,
];
$data = static::Load($mi_id);
if (is_null($data)) {
$payload['message'] = 'invalid item id';
return $payload;
}
$ext = ($p = strrpos($file['name'], '.')) !== FALSE ? substr($file['name'], $p + 1) : '';
if ($ext == 'srt') { // convert srt to vtt
$content = str_replace("\r\n", "\n", file_get_contents($file['tmp_name']));
$result = "WEBVTT\nKind: captions\nLanguage: en\n\n";
$content = explode("\n", $content);
foreach($content as $index => $line) {
$next = $content[$index+1] ?? '';
if (preg_match('~^(\d+)$~', $line) !== FALSE && strpos($next, '-->') !== FALSE) {
continue;
}
if (strpos($line, '-->') !== FALSE) {
$line = str_replace(',', '.', $line);
}
$result .= "$line\n";
}
file_put_contents($file['tmp_name'], $result);
$ext = 'vtt';
} elseif ($ext == 'json') { // this is a caption file generated by Whisper
$json = json_decode(file_get_contents($file['tmp_name']), TRUE);
$segments = $json['segments'];
$result = "WEBVTT\nKind: captions\nLanguage: en\n\n";
// convert to VTT
foreach($segments as $index => $segment) {
$timestamp = isset($segments[$index + 1]) ? $segments[$index + 1]['start'] : 86400;
$result .= $this->AsVTT($segment['words'], $timestamp);
}
file_put_contents($file['tmp_name'], $result);
$ext = 'vtt';
}
$payload['ext'] = $ext;
switch($ext) {
case 'mp3': // save to Stoughton
if ($file['size'] > 100000) {
// save to church site
$filename = $this->source
->MmItem($data)
->getDestination(self::CHURCH, "{$mi_id}.mp3");
@unlink($filename);
copy($file['tmp_name'], $filename);
@chmod($filename, 0666);
// update database
$data->_other->original = $file['name'];
$data->present_mp3 = $data->present_mp3 | (1 << static::CHURCH);
$did = '"' . esc_sql($mi_id) . '"';
$safeother = '"' . esc_sql(json_encode($data->_other, JSON_UNESCAPED_SLASHES)) . '"';
$wpdb->Query("update mm_item set present_mp3={$data->present_mp3},mi_other={$safeother} where mi_id=$did");
$this->UpdateMediaTable($mi_id, static::CHURCH, 'mp3');
// return status: reload page
$payload = [
'result' => 'ok',
'id' => $mi_id,
'reload' => 1,
];
} else {
$payload = [
'result' => 'err',
'message' => 'file too small',
'reload' => 0,
];
}
break;
case 'mp4': // save to ARIES. Creating poster/thumbnail is a separate step
$filename = $this->source
->MmItem($data)
->getDestination(static::ARIES, "{$mi_id}.mp4");
@unlink($filename);
copy($file['tmp_name'], $filename);
@chmod($filename, 0666);
$data->_other->original = str_replace('.nomark.', '.', $file['name']);
$data->present_mp4 = $data->present_mp4 | (1 << static::ARIES);
$did = '"' . esc_sql($mi_id) . '"';
$safeother = '"' . esc_sql(json_encode($data->_other, JSON_UNESCAPED_SLASHES)) . '"';
$wpdb->Query("update mm_item set present_mp4={$data->present_mp4},mi_other={$safeother} where mi_id=$did");
$this->UpdateMediaTable($mi_id, static::ARIES, 'mp4');
$this->_command_calendar_link($mi_id); // change the calendar link, if present
$payload = [
'result' => 'ok',
'id' => $mi_id,
'reload' => 1,
];
break;
case 'vtt': // save to aries
$filename = $this->source
->MmItem($data)
->getDestination(static::ARIES, "{$mi_id}.vtt");
@unlink($filename);
copy($file['tmp_name'], $filename);
@chmod($filename, 0666);
$data->present_vtt = $data->present_vtt | (1 << static::ARIES);
$did = '"' . esc_sql($mi_id) . '"';
$wpdb->Query("update mm_item set present_vtt={$data->present_vtt} where mi_id=$did");
$this->UpdateMediaTable($mi_id, static::ARIES, 'vtt');
// save to church
$filename = $this->source
->MmItem($data)
->getDestination(static::CHURCH, "{$mi_id}.vtt");
@unlink($filename);
copy($file['tmp_name'], $filename);
@chmod($filename, 0666);
$data->present_vtt = $data->present_vtt | (1 << static::CHURCH);
$did = '"' . esc_sql($mi_id) . '"';
$wpdb->Query("update mm_item set present_vtt={$data->present_vtt} where mi_id=$did");
$this->UpdateMediaTable($mi_id, static::CHURCH, 'vtt');
// return status: reload page
$payload = [
'result' => 'ok',
'id' => $mi_id,
'reload' => 1,
];
break;
case 'htm': // inline html (eg links to other sites)
case 'html': // "
case 'zip': // attachments
if ($ext == 'htm') $ext = 'html';
$filename = $this->source
->MmItem($data)
->getDestination(static::ARIES, "{$mi_id}.$ext");
@unlink($filename);
copy($file['tmp_name'], $filename);
@chmod($filename, 0666);
$this->UpdateMediaTable($mi_id, static::ARIES, $ext);
// return status: reload page
$payload = [
'result' => 'ok',
'id' => $mi_id,
'reload' => 1,
'target' => $filename,
];
break;
case 'png':
$image = imagecreatefrompng($file['tmp_name']);
$bg = imagecreatetruecolor(imagesx($image), imagesy($image));
imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); // this is the background color used by transparent areas
imagealphablending($bg, TRUE);
imagecopy($bg, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
imagedestroy($image);
$quality = 80; // 0 = worst / smaller file, 100 = better / bigger file
imagejpeg($bg, $file['tmp_name'], $quality);
imagedestroy($bg);
$this->savePoster($data, file_get_contents($file['tmp_name']));
$payload = [
'result' => 'ok',
'id' => $mi_id,
'reload' => 1,
];
break;
case 'jpg': // poster is saved to alcor
$this->savePoster($data, file_get_contents($file['tmp_name']));
$payload = [
'result' => 'ok',
'id' => $mi_id,
'reload' => 1,
];
break;
}
return $payload;
}
protected function UpdateMediaTable($mi_id, $source, $ext) {
$body = $this->_getFileInfo($source, [$mi_id]);
if ($ext == 'm4a') {
$ext = 'mp3';
$iteminfo = ['ext' => 'm4a'];
} elseif (isset($body[$mi_id][$ext])) {
$iteminfo = $body[$mi_id][$ext];
} elseif ($ext == 'mp3' && isset($body[$mi_id]['m4a'])) {
$iteminfo = $body[$mi_id]['m4a'];
$iteminfo['ext'] = 'm4a';
} else {
$iteminfo = []; // unable to get file information - certificate, [or intermediate cert] has expired
}
$this->_action_fileinfo_update_mm_media($source, $mi_id, $ext, $iteminfo);
return [
'body' => $body,
'iteminfo' => $iteminfo,
];
}
protected function WriteMP3tags($mi_id, $source, $post) {
$param = '';
foreach($post as $key => $value) {
if ($param != '') $param .= '&';
$param .= rawurlencode($key) . '=' . rawurlencode($value);
}
$data = static::Load($mi_id);
$this->source ->MmItem($data)->Load();
$auth = $this->source->generateAuth('tag');
$baseurl = $this->source->getHost($source);
$url = "$baseurl/info/$auth/mp3tags?" . $param;
$response = wp_remote_get($url, ['sslverify' => FALSE]);
$body = wp_remote_retrieve_body( $response );
$body = json_decode($body, TRUE);
}
function exitXMLerror($xml_parser, $data) {
$original = explode("\n", $data);
$linenum = xml_get_current_line_number($xml_parser);
$st = $linenum - 10; $fin = $linenum + 10;
if ($st < 1) $st = 1;
if ($fin > count($original)) $fin = count($original);
echo '<hr>';
for ($i = $st; $i <= $fin; $i++) {
$res = htmlspecialchars($original[$i-1]);
if ($i == $linenum) $res = "<font color=\"#cc0000\">$res</font>";
echo "<strong>$i.</strong> $res<br>\n";
}
echo "<br>\n";
echo sprintf("XML error: %s at line %d, col %d",
xml_error_string(xml_get_error_code($xml_parser)),
$linenum, xml_get_current_column_number($xml_parser));
echo '<hr>';
echo nl2br( htmlentities( var_export($data, 1) ) );
exit;
}
private $currentnode;
private $result;
private $index;
private $nodenum;
private $cnode;
private $characterText;
function __startElement($parser, $name, $attrs) {
$this->nodenum++; $parent = $this->cnode; $this->cnode = $this->nodenum;
$this->index[$this->cnode] = [];
$this->index["p{$this->cnode}"] = $parent;
foreach ($attrs as $k => $v) {
$this->index[$this->cnode][$k] = $v;
}
if (!isset($this->index[$parent][$name])) {
$this->index[$parent][$name] = array();
}
$this->index[$parent][$name][] = &$this->index[$this->cnode];
$this->currentnode = &$this->index[$this->cnode];
$this->characterText = '';
}
function __characterData($parser, $data) {
$this->characterText .= $data;
}
function __endElement($parser, $name) {
$this->characterText = trim($this->characterText);
if ($this->characterText != '') {
$this->index[$this->cnode]['@'] = $this->characterText;
$this->characterText = '';
}
$this->cnode = $this->index["p{$this->cnode}"];
if ($this->cnode > -1) {
$this->currentnode = &$this->index[$this->cnode];
} else {
$this->currentnode = NULL;
}
}
function XMLparser($data, $dieonerror = 1) {
$data = str_replace('&', '&', $data);
$xml_parser = xml_parser_create();
xml_set_element_handler($xml_parser, [$this, '__startElement'], [$this, '__endElement']);
xml_set_character_data_handler($xml_parser, [$this, '__characterData']);
$this->characterText = '';
$this->index = [];
$this->cnode = $this->nodenum = 0;
$this->index[$this->cnode] = [];
$index["p{$this->cnode}"] = -1;
$currentnode = &$this->index[$this->cnode];
$this->result = &$this->index[$this->cnode];
if (!xml_parse($xml_parser, $data, 1)) {
if ($dieonerror) exitXMLerror($xml_parser, $data);
}
xml_parser_free($xml_parser);
return $this->result;
}
private function savePoster($item, $imagedata, $thumb = NULL) {
global $wpdb;
$filename_final = $this->source->getDestination(static::ARIES, $item->mi_id . '.jpg');
file_put_contents($filename_final, $imagedata);
if (is_null($thumb)) {
$thumb = $this->_generateThumbnail($filename_final);
}
$item->present_jpg |= 1 << static::ARIES;
$did = '"' . esc_sql($item->mi_id) . '"';
$dthumb = '"' . esc_sql($thumb) . '"';
$sql = "update mm_item set present_jpg={$item->present_jpg},mi_thumbnail={$dthumb} where mi_id=$did";
$wpdb->Query($sql);
$this->UpdateMediaTable($item->mi_id, static::ARIES, 'jpg');
}
private function _preparev($sequence, $item, $date, $eventtype) {
global $wpdb;
// load by sequence
$data = static::Load(["mixe_sequence=$sequence", 'mixe_event=' . static::EXHORT]);
$isNew = is_null($data);
if ($isNew) {
$data = new StdClass;
$data->mi_id = $this->generateMediaHash();
$data->mi_type = 'ex';
$data->mi_date = str_replace('.', '-', $date);
$data->present_jpg = 0;
}
// update speaker, creating new entry if needed
$data->mi_speaker = $this->retrieveSpeakerId($item['SPEAKER']);
$types = ['%s', '%s', '%s', '%d'];
$values = [
'mi_id' => $data->mi_id,
'mi_type' => $data->mi_type,
'mi_date' => $data->mi_date,
'mi_speaker' => $data->mi_speaker,
];
// save mm_item
if ($isNew) {
$wpdb->insert('mm_item', $values, $types);
$this->addMixie(static::EXHORT, $sequence, $data->mi_id); // to fix: B/C also uses preparev
} else {
unset($values['mi_id']);
array_shift($types);
$result = $wpdb->update('mm_item', $values, ['mi_id' => $data->mi_id], $types);
}
if (isset($item['THUMBNAIL'])) {
$this->savePoster($data, base64_decode($item['THUMBNAIL']));
}
return $data->mi_id;
}
private function processPrepareV($name, $filename) {
// $name contains the date!
if (!preg_match('~(\d{4}).(\d{2}).(\d{2})~', $name, $matches)) {
die('uploaded filename does not contain a date');
}
$yy = (int) $matches[1];
$mm = intval($matches[2], 10);
$dd = intval($matches[3], 10);
$data = file_get_contents($filename);
$xml = $this->XMLparser($data);
if (isset($xml['XML'][0]['EVENT'])) {
foreach($xml['XML'][0]['EVENT'] as $item) {
if ($item['EVENT_TYPE'] == 60 || $item['EVENT_TYPE'] == 100) {
$sequence = $yy * 10000 + $mm * 100 + $dd;
if ($item['EVENT_TYPE'] == 100) $sequence++;
$this->_preparev($sequence, $item, $matches[0], $item['EVENT_TYPE']); // eventtype will be deprecated
}
}
}
$payload = [
'result' => 'ok',
'message' => 'uploaded',
'reload' => 1,
];
header('Content-type: application/json');
die(json_encode($payload, JSON_UNESCAPED_SLASHES));
}
private function _command_upload_item($mi_id, $file) {
global $wpdb;
if (!is_uploaded_file($file['tmp_name'])) {
die('something is wrong');
}
if (empty($mi_id)) {
$ext = ($p = strrpos($file['name'], '.')) !== FALSE ? substr($file['name'], $p + 1) : '';
switch($ext) {
case 'xml': // don't save, but extract the info
$this->processPrepareV($file['name'], $file['tmp_name']);
exit;
default:
die('item cannot be empty');
}
}
$mi_id = str_replace(['..', '/', '\\'], '', $mi_id);
if (array_key_exists('chunks', $_POST)) {
// upload this chunk to the wordpress upload folder
// the LAST chunk will take a long time to load - the entire file is copied to Hostz.
$targetname = realpath(__DIR__ . '/../../uploads/plupload') . '/' . $mi_id;
$this->_uploadChunk($file, $targetname, function($uploadedfile) use ($file, $mi_id) {
// when complete, move it to HOSTZ
$file['tmp_name'] = $uploadedfile;
$file['name'] = wp_unslash($_POST['name']);
$file['size'] = filesize($uploadedfile);
$res = $this->_uploadComplete($mi_id, $file);
unlink($file['tmp_name']);
return $res;
});
}
$payload = $this->_uploadComplete($mi_id, $file);
header('Content-type: application/json');
die(json_encode($payload, JSON_UNESCAPED_SLASHES));
}
// this is too slow! Must be run on cdn server
private function _command_download_for_youtube($mi_id) {
global $wpdb;
$data = static::Load($mi_id);
if (is_null($data)) {
die('invalid item id');
}
// locate mp3
$svr = $this->source
->MmItem($data)
->determineBestServer($data->present_mp3);
if (is_null($svr)) {
die('Unable to locate source file');
}
$filename_source = $this->source->getDestination($svr, "{$mi_id}.mp3");
// use ffmpeg + thumbnail to create video
$filename_temp = tempnam(sys_get_temp_dir(), 'mp4') . '.mp4';
// download video - use content-disposition to download/filename
// delete temp file
if ($data->mi_type == 'mt') {
$background = 'background-blu.jpg';
} else {
$background = 'background-grn.jpg';
}
$safe_bg = escapeshellarg(__DIR__ . '/cdn/include/' . $background);
$safe_in = escapeshellarg($filename_source);
$command = self::FFMPEG . " -loop 1 -i $safe_bg -i $safe_in -c:a copy -c:v libx264 -shortest $filename_temp";
header('Content-type: text/plain');
var_dump($mi_id);
echo "$command\n";
echo `$command`;
exit;
}
private function report_missingitems() {
global $wpdb;
$sql = <<<BLOCK
select mi_id,mi_title,ms_firstname,ms_lastname,ms_suffix,me_year,me_location,
me_topic,mixe_sequence,present_mp3,present_mp4,present_vtt
from mm_item_x_event as x
inner join mm_item as i on x.mixe_item = i.mi_id
left join mm_speaker as s on s.ms_id = i.mi_speaker
left join mm_event as e on e.me_id = mixe_event
where ((present_mp3 = 0 and present_mp4 = 0) or present_vtt=0)
order by me_year,me_location,me_topic,mixe_sequence
BLOCK;
$items = $wpdb->get_results( $sql );
$output = '';
foreach($items as $item) {
if (empty($item->present_mp3) && empty($item->present_mp4)) {
$status = 'missing audio or video';
} elseif (empty($item->present_vtt)) {
$status = 'missing caption';
} else {
$status = '';
}
$output .= <<<BLOCK
<tr>
<td><a href="?page=talks-single&action=class&cid={$item->mi_id}" target="newpage">{$item->mi_id}</a></td>
<td>{$item->ms_firstname} {$item->ms_lastname}</td>
<td>{$item->mi_title}</td>
<td>{$item->me_topic} ({$item->mixe_sequence})</td>
<td>{$item->me_location} ({$item->me_year})</td>
<td>$status</td>
</tr>
BLOCK;
}
echo <<<BLOCK
<h3>Missing Items</h3>
<table>
$output
</table>
BLOCK;
}
/* public function repairdatabase() {
global $wpdb;
$sql = "select mi_id,mi_duration,mi_other from mm_item where mi_duration is null";
$items = $wpdb->get_results( $sql );
foreach($items as $item) {
$item->other = json_decode($item->mi_other);
echo "{$item->mi_id}...<br>\n";
// echo '<pre>' . htmlspecialchars(var_export($item, 1)) . '</pre>';
if (!empty($item->other->duration)) {
$did = '"' . esc_sql($item->mi_id) . '"';
$wpdb->Query("update `mm_item` set mi_duration={$item->other->duration} where mi_id=$did");
}
}
die('Done');
} */
private function addMixie($event, $sequence, $item) {
global $wpdb;
$devent = '"' . esc_sql($event) . '"';
$dsequence = '"' . esc_sql($sequence) . '"';
$ditem = '"' . esc_sql($item) . '"';
$sql = "insert into `mm_item_x_event` (mixe_event, mixe_sequence, mixe_item) VALUES ($devent, $dsequence, $ditem) ON DUPLICATE KEY UPDATE mixe_item=$ditem";
$wpdb->Query($sql);
}
// Returns a file size limit in bytes based on the PHP upload_max_filesize
// and post_max_size
public function file_upload_max_size() {
static $max_size = -1;
if ($max_size < 0) {
// Start with post_max_size.
$post_max_size = $this->parse_size(ini_get('post_max_size'));
if ($post_max_size > 0) {
$max_size = $post_max_size;
}
// If upload_max_size is less, then reduce. Except if upload_max_size is
// zero, which indicates no limit.
$upload_max = $this->parse_size(ini_get('upload_max_filesize'));
if ($upload_max > 0 && $upload_max < $max_size) {
$max_size = $upload_max;
}
}
return $max_size;
}
private function parse_size($size) {
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
$size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
if ($unit) {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
}
else {
return round($size);
}
}
protected function verbose($ok=1,$info='', $param = NULL){
if (!$ok) {
http_response_code(400);
}
$payload = [
'ok' => $ok,
'info' => $info,
];
if ($param) $payload['param'] = $param;
die(json_encode($payload, JSON_UNESCAPED_SLASHES));
}
protected function _uploadChunk($FILE, $targetname, $callback) {
if ($FILE['error']) {
$this->verbose(0, "Failed to move uploaded file.");
}
$chunk = isset($_POST['chunk']) ? intval($_POST['chunk']) : 0;
$chunks = isset($_POST['chunks']) ? intval($_POST['chunks']) : 0;
$out = @fopen("{$targetname}.part", $chunk ? 'ab' : 'wb');
if ($out) {
$buff = file_get_contents($FILE['tmp_name']);
fwrite($out, $buff);
@fclose($out);
@unlink($FILE['tmp_name']);
} else {
$this->verbose(0, "Failed to open output stream", $targetname);
}
$res = NULL;
if (!$chunks || $chunk == $chunks - 1) {
rename("{$targetname}.part", $targetname);
if (is_callable($callback)) {
$res = $callback($targetname);
}
}
$this->verbose(1, "Upload OK", $res);
}
private function _generateThumbnail($filename_source) {
if (file_exists($filename_source)) {
$image_info = getimagesize($filename_source);
$wd = $image_info[0];
$ht = $image_info[1];
$new_height = 110;
$new_width = intval($wd * $new_height / $ht);
$original = ImageCreateFromJPEG($filename_source);
$image = imagecreatetruecolor($new_width, $new_height);
imagecopyresized($image, $original, 0, 0, 0, 0, $new_width, $new_height, $wd, $ht);
imagedestroy($original);
// create the image tag, with size, class, and data
ob_start();
imagejpeg($image, NULL, 90);
$rawImageBytes = base64_encode(ob_get_clean());
imagedestroy($image);
return "<img width=\"$new_width\" height=\"$new_height\" src=\"data:image/jpeg;base64,$rawImageBytes\">";
}
return '';
}
private function _ImageTothumbnail($imagedata) {
$rawImageBytes = base64_encode($imagedata);
$image = ImageCreateFromString($imagedata);
$new_width = imagesx($image);
$new_height = imagesy($image);
imagedestroy($image);
return "<img width=\"$new_width\" height=\"$new_height\" src=\"data:image/jpeg;base64,$rawImageBytes\">";
}
private function _hasoldThumbFile($data) {
/* $basefile = str_replace('-', '.', $data->mi_date);
list($yy,$mm,$dd) = explode('.', $basefile);
$yy = intval($yy);
$mm = intval($mm, 10);
$dd = intval($dd, 10);
$sequence = $yy * 10000 + $mm * 100 + $dd;
$event = ($data->mixe_sequence == $sequence) ? 60 : 100;
$filename = "/var/www/hopeinstoughton_www/file_up/media/{$basefile}_{$event}.jpg";
if (file_exists($filename)) {
return $filename;
} */
return FALSE;
}
private function _command_trash_item($auth, $source, $type) {
global $wpdb, $MEDIASEARCH;
header('Content-type: text/plain');
$param = $this->source->decodeAuth($auth);
if (!$param->valid) {
die("Invalid Link");
}
if ($param->expired) {
die("Expired Link");
}
$data = static::Load($param->id, ['media' => TRUE]);
$found = NULL;
// is it in mm_media? exit if not
foreach($data->_media as $media_item) {
if ($media_item->mm_source == $source && $media_item->mm_type == $type) {
$found = $media_item;
break;
}
}
if (is_null($found)) {
die('media item not found');
}
// delete the file
$target = $this->source->getDestination($found->mm_source, "{$data->mi_id}.{$found->mm_type}"); // delete from filesystem
@unlink($target);
// remove from mm_media and update flags
$did = '"' . esc_sql($data->mi_id) . '"';
$dsource = '"' . esc_sql($source) . '"';
$dtype = '"' . esc_sql($type) . '"';
$wpdb->Query("delete from mm_media where mm_hash=$did and mm_source=$dsource and mm_type=$dtype");
// update flags
$mask = 1 << $source;
switch($type) {
case 'mp4':
$wpdb->Query("update mm_item set present_mp4 = present_mp4 & ~ $mask where mi_id=$did");
break;
case 'jpg':
$wpdb->Query("update mm_item set present_jpg = present_jpg & ~ $mask where mi_id=$did");
break;
case 'vtt':
$wpdb->Query("update mm_item set present_vtt = present_vtt & ~ $mask where mi_id=$did");
break;
case 'mp3':
$wpdb->Query("update mm_item set present_mp3 = present_mp3 & ~ $mask where mi_id=$did");
break;
}
header("Location: /wp-admin/admin.php?page=talks-single&action=class&cid={$data->mi_id}");
exit;
}
// this will not remove the entry from mm_event
private function _command_delete_talk($auth) {
global $wpdb, $MEDIASEARCH;
header('Content-type: text/plain');
$param = $this->source->decodeAuth($auth);
if (!$param->valid) {
die("Invalid Link");
}
if ($param->expired) {
die("Expired Link");
}
$data = static::Load($param->id, ['media' => TRUE]);
if (is_null($data)) {
die("item \"{$param->id}\" could not be found");
}
if (is_object($MEDIASEARCH)) {
$MEDIASEARCH->remove_from_xapian($data->mi_id); // this does not remove
}
// remove from each cdn - mm_media
$dhash = '"' . $data->mi_id . '"';
foreach($data->_media as $entry) {
$target = $this->source->getDestination($entry->mm_source, "{$data->mi_id}.{$entry->mm_type}"); // delete from filesystem
@unlink($target);
$dsource = '"' . $entry->mm_source . '"';
$dtype = '"' . $entry->mm_type . '"';
$wpdb->Query("delete from mm_media where mm_hash=$dhash and mm_source=$dsource and mm_type=$dtype");
}
$wpdb->Query("delete from mm_item where mi_id=$dhash"); // remove from database - mm_item
$wpdb->Query("delete from mm_item_x_event where mixe_item=$dhash"); // remove from database - mm_item_x_event
return $data;
}
private function _command_refresh_fileinfo($auth, $server) {
header('Content-type: text/plain');
$param = $this->source->decodeAuth($auth);
if (!$param->valid) {
die("Invalid Link");
}
if ($param->expired) {
die("Expired Link");
}
$ext = $param->ext;
$data = static::Load($param->id);
if (is_null($data)) {
die("Entry has been deleted");
}
if ($this->isPost) {
$post = [
'artist' => trim($_POST['artist']),
'album' => trim($_POST['album']),
'title' => trim($_POST['title']),
'year' => trim($_POST['year']),
'track' => (int) trim($_POST['track']),
];
switch($server) {
case 3:
if ($ext == 'mp3')
$this->WriteMP3tags($param->id, static::CHURCH, $post);
break;
}
}
switch($server) {
case 1:
$this->UpdateMediaTable($param->id, static::ARIES, $ext);
break;
case 3:
$this->UpdateMediaTable($param->id, static::CHURCH, $ext);
break;
case 4:
$this->UpdateMediaTable($param->id, static::HOME, $ext);
break;
}
if ($this->isPost) {
$output = [
'success' => 'unknown',
];
header('Content-type: application/json');
die(json_encode($output, JSON_UNESCAPED_SLASHES));
}
header("Location: /wp-admin/admin.php?page=talks-single&action=class&cid=" . $param->id);
exit;
}
private function _command_generate_thumb($auth) {
global $wpdb;
header('Content-type: text/plain');
$param = $this->source->decodeAuth($auth);
if (!$param->valid) {
die("Invalid Link");
}
if ($param->expired) {
die("Expired Link");
}
$data = static::Load($param->id);
if ($data->mi_type == 'ex') { // use the xml file if it is present
$filename_src = $this->_hasoldThumbFile($data);
if ($filename_src === FALSE) {
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $param->id . '&info=thumbnail+missing');
exit;
}
// save the jpeg to ALCOR
$filename_final = $this->source->getDestination(static::ARIES, $param->id . '.jpg');
@unlink($filename_final);
copy($filename_src, $filename_final);
@chmod($filename_final, 0666);
$thumb = $this->_generateThumbnail($filename_src);
} else {
$svr = $this->source->determineBestServer($data->present_mp4);
$filename_src = $this->source->getDestination($svr, $param->id . '.mp4');
$temp = tempnam(sys_get_temp_dir(), 'jpg');
$filename_temp = $temp . '.jpg';
@unlink($filename_temp);
$dsource = escapeshellarg($filename_src);
$dtarget = escapeshellarg($filename_temp);
// run ffmpeg to extract very first frame
$command = static::FFMPEG . " -i $dsource -vframes 1 $dtarget";
$output = `$command 2>&1`;
file_put_contents('/phil/ffmpeg.txt', $command . "\n\n" . $output);
// save the jpeg to ALCOR
$filename_final = $this->source->getDestination(static::ARIES, $param->id . '.jpg');
@unlink($filename_final);
copy($filename_temp, $filename_final);
@chmod($filename_final, 0666);
// create a thumb
$thumb = $this->_generateThumbnail($filename_temp);
@unlink($filename_temp);
@unlink($temp);
}
// update database
$data = static::Load($param->id);
$data->present_jpg |= 1 << static::ARIES;
$did = '"' . esc_sql($param->id) . '"';
$dthumb = '"' . esc_sql($thumb) . '"';
$sql = "update mm_item set present_jpg={$data->present_jpg},mi_thumbnail={$dthumb} where mi_id=$did";
$wpdb->Query($sql);
// get file info
$this->UpdateMediaTable($data->mi_id, static::ARIES, 'jpg');
// redirect back to page
header('Location: /wp-admin/admin.php?page=talks-single&action=class&cid=' . $param->id);
exit;
}
private function _command_validate_slug($me_id) {
global $wpdb;
$slug = $_GET['slug'];
$did = '"' . esc_sql($me_id) . '"';
$dslug = '"' . esc_sql(trim($_GET['slug'])) . '"';
$row = $wpdb->get_row("select me_slug from mm_event where me_slug=$dslug and me_id<>$did");
$result = [
'status' => $row === NULL ? 1 : 0,
];
header('Content-type: application/json');
die(json_encode($result, JSON_UNESCAPED_SLASHES));
}
private function _getFileInfo($source, $ids) {
$baseurl = $this->source->getHost($source);
$url = "$baseurl/info?id=" . join('.', $ids);
$response = wp_remote_get($url, ['sslverify' => FALSE]);
$body = wp_remote_retrieve_body( $response );
$body = json_decode($body, TRUE);
return $body;
}
private function action_fileinfo($source) {
global $wpdb;
// get a list of all resources that are on the specified server
$sql = <<<BLOCK
select mi_id,mi_other,present_mp3,present_vtt,present_mp4,present_jpg
from mm_item as i
where (present_mp3 <> 0 or present_vtt <> 0 or present_mp4 <> 0 or present_jpg <> 0)
BLOCK;
$resource = [];
$items = $wpdb->get_results( $sql );
$current = 1 << $source;
set_time_limit(0);
foreach($items as $item) {
$servers = (int) $item->present_mp3 | (int) $item->present_mp4 | (int) $item->present_jpg | (int) $item->present_vtt;
if ($servers & $current) {
$resource[] = $item->mi_id;
}
}
// then, in blocks of 16, get the info
while (count($resource)) {
$slice = array_splice($resource, 0, 16);
echo join(', ', $slice) . "<br>\n";
flush();
$body = $this->_getFileInfo($source, $slice);
// update the database
foreach($body as $id => $id_info) {
foreach($id_info as $ext => $iteminfo) {
$this->_action_fileinfo_update_mm_media($source, $id, $ext, $iteminfo);
}
}
}
// finally remove old entries
}
private function _action_fileinfo_update_mm_media($source, $id, $ext, $iteminfo) {
global $wpdb;
$dsource = (int) $source;
$dhash = '"' . esc_sql($id) . '"';
$dtype = '"' . esc_sql($ext) . '"';
// fields to keep:
$other = [];
foreach(['bitrate', 'frequency', 'topic', 'title', 'speaker', 'year', 'track', 'ext'] as $fieldname) {
if (isset($iteminfo[$fieldname])) {
$other[$fieldname] = $iteminfo[$fieldname];
}
}
$types = ['%s', '%d', '%s', '%d', '%s', '%d', '%s'];
$values = [
'mm_hash' => $id,
'mm_source' => $source,
'mm_type' => $ext,
'mm_filesize' => $iteminfo['filesize'] ?? 0,
'mm_framesize' => isset($iteminfo['framesize']) ? $iteminfo['framesize'] : NULL,
'mm_duration' => isset($iteminfo['duration']) ? $iteminfo['duration'] : NULL,
'mm_other' => json_encode($other, JSON_UNESCAPED_SLASHES),
];
$sql = "select mm_filesize,mm_duration,mm_framesize,mm_other from mm_media where mm_hash=$dhash and mm_source=$dsource and mm_type=$dtype";
$data = $wpdb->get_row($sql);
if (is_null($data)) {
$wpdb->insert('mm_media', $values, $types);
return;
}
// has something changed?
if ($data->mm_filesize != $values['mm_filesize'] || $data->mm_duration != $values['mm_duration'] || $data->mm_other != $values['mm_other']) {
unset($values['mm_hash'], $values['mm_source'], $values['mm_type']);
array_shift($types);
array_shift($types);
array_shift($types);
$wpdb->update('mm_media', $values, ['mm_hash' => $id, 'mm_source' => $source, 'mm_type' => $ext], $types);
}
}
public function __wp_page() {
global $post, $wpdb;
// this plug intercepts the text '[videoplayer action="latest-class"]' within a post/page - the page can only contain one such entry
$hasPlayer = preg_match('~\[videoplayer(.*?)\]~ims', $post->post_content, $match) ? $match[0] : '';
if (strpos($hasPlayer, 'action="latest-class"') !== FALSE) {
$transcript = preg_match('~transcript="(.*?)"~', $hasPlayer, $match) ? $match[1] : FALSE;
$sql = <<<BLOCK
select mi_id, mi_date, mi_title,
ms_firstname, ms_lastname, ms_suffix,
me_slug, me_topic
from mm_item_x_event as x
inner join mm_item as i on i.mi_id=x.mixe_item
inner join mm_event as e on e.me_id = x.mixe_event
left join mm_speaker as s on s.ms_id = i.mi_speaker
inner join mm_media as m on m.mm_hash = x.mixe_item and m.mm_type="mp4"
where mi_type="ac"
order by mi_date desc
limit 1
BLOCK;
$data = $wpdb->get_row($sql);
// $j = '<pre>' . htmlspecialchars(var_export($data, 1)) . '</pre>';
$j = "[videoplayer archive=\"{$data->mi_id}\" transcript=\"1\"]";
$post->post_content = str_replace($hasPlayer, $j, $post->post_content);
}
}
// https://hopeinstoughton.org/moorestown-audio/
private function _extra_json() {
global $wpdb, $MEDIASEARCH;
$events = range(366, 396);
$result = [];
foreach($events as $event) {
$data = static::Load(["me_id=$event"], ['first' => FALSE, 'media' => TRUE, 'order' => 'me_id, mixe_sequence']);
if (is_null($data)) continue;
$first = $data[0];
$node = [
'topic' => $first->me_topic,
'speaker' => static::AsSpeaker($first),
'location' => $first->me_location,
'year' => $first->me_year,
];
if (is_object($MEDIASEARCH)) {
$node['media'] = $MEDIASEARCH->generateAuth($first->mi_type, $first->mi_id, TRUE) . '#' . $first->mi_type . '_' .rawurlencode($first->mi_id);
}
// echo "<pre>" . htmlspecialchars(var_export($media, 1)) . "</pre>";
$max = 0;
foreach($data as $item) {
if ($item->mixe_sequence > $max) $max = $item->mixe_sequence;
}
foreach($data as $item) {
$other = json_decode($item->mi_other);
list($duration, $filesize) = $this->_extra_json_deets($item->_media);
$node['talks'][] = [
'title' => $item->mi_title,
'class' => "{$item->mixe_sequence} of $max",
'duration' => $duration,
'filesize' => $filesize,
'download' => apply_filters('mm_generate_link', '', $item->mi_id, $other->original, $item->present_mp3),
// filesize, duration
];
}
$result[] = $node;
}
return $result;
}
private function _extra_json_deets($media) {
foreach($media as $item) {
if ($item->mm_type == 'mp3' || $item->mm_type == 'mp4') {
$filesize = (int) $item->mm_filesize;
$duration = (int) $item->mm_duration;
return [$duration, $filesize];
}
}
return [0, 0];
}
public function _shortcode_extras() {
$result = $this->_extra_json();
$output = '';
foreach($result as $entry) {
$ms = htmlspecialchars($entry['topic']);
if (isset($entry['media'])) {
$view = "\n<tr><td>Online:</td><td><a href=\"{$entry['media']}\" target=\"listennow\">Listen Now</a></td></tr>";
} else {
$view = '';
}
$output .= <<<BLOCK
<tr><td colspan="5">
<table style="width:100%;border-radius:5px;border:1px solid #999;padding:10px;background-color:#eee">
<tr><td width="120">Location:</td><td>{$entry['location']}</td></tr>
<tr><td>Topic:</td><td>$ms</td></tr>
<tr><td>Speaker:</td><td>{$entry['speaker']}</td></tr>
<tr><td>Year:</td><td>{$entry['year']}</td></tr>$view
</table>
</td></tr>
BLOCK;
foreach($entry['talks'] as $talk) {
$title = htmlspecialchars($talk['title']);
$duration = static::asHMS($talk['duration']);
$size = MediaSource::formatBytes($talk['filesize']);
$output .= <<<BLOCK
<tr>
<td width="370">$title</td>
<td width="70">{$talk['class']}</td>
<td width="70" style="text-align:right;padding-right:10px">{$duration}</td>
<td width="90" style="text-align:right;padding-right:10px">{$size}</td>
<td><a href="{$talk['download']}">download</a></td>
</tr>
BLOCK;
}
$output .= '<tr><td colspan="3"> </td></tr>';
}
// $x = '<pre>' . htmlspecialchars(var_export($result, 1)) . '</pre>';
return <<<BLOCK
<table style="width:700px">
$output
</table>
BLOCK;
}
// get a list of $qty files that are not on the requested $cdn.
// returns a list containing download links for mp3, mp4, jpg or vtt files
// run once per hour
private function Synchronize($cdn, $qty = 50) {
global $wpdb;
$bitmap = 1 << $cdn;
$where = "((present_mp3 <> 0) and (present_mp3 & $bitmap = 0)) or ((present_mp4 <> 0) and (present_mp4 & $bitmap = 0)) or ((present_vtt <> 0) and (present_vtt & $bitmap = 0)) or ((present_jpg <> 0) and (present_jpg & $bitmap = 0))";
$list = static::Load(
[$where],
['first' => FALSE, 'media' => TRUE, 'limit' => $qty, 'order' => 'rand()']
);
header('Content-type: text/plain');
$fieldnames = ['present_mp3', 'present_mp4', 'present_vtt', 'present_jpg']; // mp3 present may also be true for m4a
$results = [];
foreach($list as $item) {
$this->source->MmItem($item);
$filename = pathinfo($item->_other->original, PATHINFO_FILENAME);
foreach($fieldnames as $fieldname) {
$bmp = (int) $item->{$fieldname};
if ($bmp && !($bmp & $bitmap)) {
list($filler, $type) = explode('_', $fieldname);
// get the real extension, if one is present
$existing = NULL;
$ext = $type;
foreach($item->_media as $mediaitem) {
if ($mediaitem->mm_type == $type) {
$existing = $mediaitem->mm_other;
break;
}
}
if (!empty($existing) && !empty($existing->ext)) {
$ext = $existing->ext;
}
$itemfilename = $type == 'vtt' ? $item->mi_id . '.vtt' : $filename . '.' . $ext;
$server = $this->source->determineBestServer($bmp);
$link = $this->source->generateDownloadLink($server, $itemfilename);
$filesize = $this->_synchronize_getFilesize($item->_media, $type);
$results[] = [
'type' => $ext,
'id' => $item->mi_id,
'fs' => $filesize,
'link' => $link,
];
}
}
}
if (0) {
// when a certificate has expired. curl will be unable to download the source, resulting in a zero-length file. This will update them
// get all the affected files
$hashes = $wpdb->get_col("select distinct mm_hash from mm_media where `mm_source`={$cdn} and `mm_filesize` IS NULL");
if (count($hashes)) {
$hashes = join(',', array_map( function($x) {
return '"' . esc_sql($x) . '"';
}, $hashes ));
$list = static::Load(
[ 1, "mixe_item in ($hashes)" ],
[ 'first' => FALSE, 'media' => TRUE, 'limit' => 999 ]
);
if (is_array($list)) {
foreach($list as $item) {
$this->source->MmItem($item);
$filename = pathinfo($item->_other->original, PATHINFO_FILENAME);
foreach($fieldnames as $fieldname) {
$bmp = (int) $item->{$fieldname};
if ($bmp) {
list($filler, $type) = explode('_', $fieldname);
$itemfilename = $type == 'vtt' ? $item->mi_id . '.vtt' : $filename . '.' . $type;
$server = $this->source->determineBestServer($bmp & ~(1 << $cdn)); // make sure we are downloading the missing version
if ($server !== NULL) {
$link = $this->source->generateDownloadLink($server, $itemfilename);
$filesize = $this->_synchronize_getFilesize($item->_media, $type);
$results[] = [
'type' => $type,
'id' => $item->mi_id,
'fs' => $filesize,
'link' => $link,
];
}
}
}
}
}
}
}
return $results;
}
private function _synchronize_getFilesize($media, $type) {
foreach($media as $item) {
if ($item->mm_type == $type) {
return (int) $item->mm_filesize;
}
}
return -1;
}
private function Synchronize_POST($cdn, $auth, $type) {
global $wpdb;
$res = $this->source->decodeAuth($auth);
if (!$res->valid) {
return [
'success' => 0,
'message' => 'Invalid authentication',
];
}
if ($type != $res->ext) {
return [
'success' => 0,
'message' => 'Invalid type specified',
];
}
// mark it as valid!
$ok2 = 1 << $cdn;
$did = '"' . esc_sql($res->id) . '"';
switch($res->ext) {
case 'vtt':
$wpdb->Query("update mm_item set present_vtt = present_vtt | $ok2 where mi_id=$did");
break;
case 'jpg':
$wpdb->Query("update mm_item set present_jpg = present_jpg | $ok2 where mi_id=$did");
break;
case 'mp4':
$wpdb->Query("update mm_item set present_mp4 = present_mp4 | $ok2 where mi_id=$did");
break;
case 'm4a':
case 'mp3':
$wpdb->Query("update mm_item set present_mp3 = present_mp3 | $ok2 where mi_id=$did");
break;
}
$this->UpdateMediaTable($res->id, $cdn, $res->ext);
return [
'success' => 1,
'message' => 'OK',
'cdn' => $cdn,
'item' => $res->id,
];
}
private function Synchronize_OldFiles($cdn, $oldfiles) {
global $wpdb;
$sql = <<<BLOCK
select mi_id,mi_type,mm_type,mm_filesize
from mm_media
inner join mm_item on mm_hash=mi_id
where mm_source = $cdn
and mm_type in ('mp3', 'mp4')
and mi_type="other"
order by mm_filesize desc
limit 10
BLOCK;
$items = $wpdb->get_results( $sql );
foreach($items as $item) {
$item->mm_filesize = intval($item->mm_filesize / 1024 / 1024);
}
return $items;
}
// this hook does not exist in newer versions of the plugin - so only the placeholders are shown
public function __carousel($html, $post_id) {
$videos = []; // array: of datetime / title / url / thumbnail
// assemble list of videos to be displayed
$this->_carousel_lastservice($videos); // current full service
$this->_carousel_liveevent($videos);
$this->_carousel_lastadultclass($videos);
$this->_carousel_lastexhort($videos);
$this->_carousel_lastbibleclass($videos);
$this->_carousel_lastonesource($videos); // Richard
// sort by newest to oldest
usort($videos, function($a, $b) {
return $b['datetime'] - $a['datetime'];
});
// generate the html
$html = $this->_carousel_generatehtml($html, $videos, $post_id);
return $html;
}
private function _carousel_lastservice(&$videos) {
global $wpdb;
// get sunday
$info = video_archive_class::getLastSundayService(TRUE);
if ($info !== FALSE) {
$dt = str_replace('.', '-', $info['date']);
$date = $dt . ' 06:00';
$videos[] = [
'datetime' => strtotime($date),
'title' => $info['speaker'],
'event' => 'Memorial Service',
'url' => '/members/current',
'thumb' => $info['thumb'],
];
}
}
// live video - only if password level is correct
private function _carousel_liveevent(&$videos) {
// are we live - is the youtube link present
$info = apply_filters('youtubeschedule-active', ['active' => 0]);
if ($info['active']) {
$videos[] = [
'datetime' => time(),
'title' => $info['desc'],
'event' => 'Live Now',
'url' => $info['link'],
'thumb' => $this->URL . '/logo_youtube.jpg',
];
}
// youtubeschedule-zoom - only public events, a different account is used for business meetings
$info = apply_filters('youtubeschedule-zoom', ['active' => 0]);
if ($info['active']) {
$videos[] = [
'datetime' => time() - 1800,
'title' => $info['desc'],
'event' => 'Live Now',
'url' => $info['link'],
'thumb' => $this->URL . '/logo_zoom.jpg',
];
}
}
private function _carousel_lastadultclass(&$videos) {
global $MEDIASEARCH;
$cutoff = date('Y-m-d', time() - 13 * 86400);
$row = static::Load(['((mi_type="ac") or (mi_type="bapt"))', 'present_mp4 <> 0', "mi_date >= \"$cutoff\""],
['order' => 'mi_date desc'] );
if ($row !== NULL) {
$date = $row->mi_date;
if (is_object($MEDIASEARCH)) {
$link = $MEDIASEARCH->generateAuth($row->mi_type, $row->mi_id, TRUE) . '#' . $row->mi_type . '_' . $row->mi_id;
} else {
$link = '#';
}
$tp = $row->mi_type;
$videos[] = [
'datetime' => strtotime("$date 09:00"),
'title' => $this->_carousel_speaker($row),
'url' => $link,
'event' => $tp == 'ac' ? 'Adult Class' : 'Baptism',
'thumb' => $this->_carousel_clean($row->mi_thumbnail),
];
}
}
private function _carousel_speaker($row) {
$name = trim($row->ms_firstname . ' ' . $row->ms_lastname);
if ($row->ms_suffix) $name .= ' ' . ucfirst($row->ms_suffix);
return $name;
}
private function _carousel_lastexhort(&$videos) { // or two
global $MEDIASEARCH;
list($yy, $mm, $dd) = explode(',', date('Y,n,j', time() + 86400));
$today = $yy * 10000 + $mm * 100 + $dd;
$rows = static::Load(['mixe_event=' . static::EXHORT, "mixe_sequence<$today"],
['first' => FALSE, 'thumb' => TRUE, 'limit' => 5, 'order' => 'mixe_sequence desc'] );
$date = FALSE;
foreach($rows as $row) {
if ($date === FALSE) $date = $row->mi_date;
if ($row->mi_date != $date)
break;
list($yy, $mm, $dd) = explode('-', $row->mi_date);
$thisdate = $yy * 10000 + intval($mm, 10) * 100 + intval($dd, 10);
// don't care about actual time, only that third exhort comes after the second.
if ($thisdate == $row->mixe_sequence) {
$time = '11:00';
$event = 'Exhort';
} else {
$time = '10:00';
$event = 'Devotion';
}
if (is_object($MEDIASEARCH)) {
$link = $MEDIASEARCH->generateAuth($row->mi_type, $row->mi_id, TRUE);
} else {
$link = '#';
}
$videos[] = [
'datetime' => strtotime("$date $time"),
'title' => $this->_carousel_speaker($row),
'url' => $link,
'event' => $event,
'thumb' => $this->_carousel_clean($row->mi_thumbnail),
];
}
return NULL;
}
private function _carousel_lastonesource(&$videos) {
global $MEDIASEARCH;
$row = static::Load(['present_mp4 <> 0', 'mixe_event = 588'],
['order' => 'mi_date desc'] );
if ($row !== NULL) {
$date = $row->mi_date;
$dt = strtotime("$date 19:30");
if ($dt > time() - 86400 * 10) {
if (is_object($MEDIASEARCH)) {
$link = $MEDIASEARCH->generateAuth('in', $row->mi_id, TRUE);
} else {
$link = '#';
}
$videos[] = [
'datetime' => $dt,
'title' => $this->_carousel_speaker($row),
'event' => 'The One Source',
'url' => $link,
'thumb' => $this->_carousel_clean($row->mi_thumbnail),
];
}
}
}
private function _carousel_lastbibleclass(&$videos) {
global $MEDIASEARCH;
$row = static::Load(['mi_type="bc"', 'present_mp4 <> 0'],
['order' => 'mi_date desc'] );
if ($row !== NULL) {
$date = $row->mi_date;
if (is_object($MEDIASEARCH)) {
$link = $MEDIASEARCH->generateAuth($row->mi_type, $row->mi_id, TRUE);
} else {
$link = '#';
}
$videos[] = [
'datetime' => strtotime("$date 19:30"),
'title' => $this->_carousel_speaker($row),
'event' => 'Bible Class',
'url' => $link,
'thumb' => $this->_carousel_clean($row->mi_thumbnail),
];
}
}
private function _carousel_clean($data) {
return preg_match('~src="(.*?)"~', $data, $match) ? $match[1] : '';
}
private function _carousel_generatehtml($html, $videos, $post_id) {
$slickoptions = preg_match("~data-slick='(.*?)'~", $html, $match) ? $match[1] : '';
// echo "<pre>" . htmlspecialchars(var_export($videos, 1)) . '</pre>';
$shortcode_data = get_post_meta( $post_id, 'sp_wpcp_shortcode_options', true );
$carousel_direction = isset( $shortcode_data['wpcp_carousel_direction'] ) ? $shortcode_data['wpcp_carousel_direction'] : '';
$xpostid = esc_attr($post_id);
$the_rtl = ( 'ltr' === $carousel_direction ) ? 'dir="rtl"' : 'dir="ltr"';
$slides = '';
foreach($videos as $video) {
$safe_title = esc_html($video['title']);
$safe_link = esc_html($video['url']);
if ($video['event'] != '') $safe_title .= '<br /><br />';
$slides .= <<<BLOCK
<div class="wpcp-single-item detail-with-overlay">
<div class="wpcp-slide-image">
<a href="{$safe_link}" target="_blank"><img class="skip-lazy" src="{$video['thumb']}" alt="{$video['title']}"></a>
</div>
<div class="wpcp-all-captions" style="pointer-events: none;">
<div class="wpcp-image-caption">
{$safe_title}
</div>
<div class="wpcp-image-event">
{$video['event']}
</div>
</div>
</div>
BLOCK;
}
return <<<BLOCK
<div class="wpcp-carousel-wrapper wpcp-wrapper-{$xpostid}">
<div id="sp-wp-carousel-free-id-$xpostid" class="wpcp-carousel-section sp-wpcp-$xpostid nav-vertical-center wpcp-image-carousel detail-with-overlay overlay-on-hover wpcp-standard" data-slick='$slickoptions' {$the_rtl}>
{$slides}
</div>
</div>
BLOCK;
}
protected function action_import_cygnus($me_id) {
// get data from cygnus - speaker, thumbnail, sequence#
$curl = new WP_Http_Curl;
$response = $curl->request('https://cygnus.hopeinstoughton.org/~/videoEdit/current');
$response = json_decode($response['body'], TRUE);
// show date. thumb and speaker and duration
// if ok to continue, create entries, copy image
// copy image
// type":"60","in":1913.117053,"out":4070.544448,"speaker":"Jim Sullivan","date":"2022.01.23","thumb":"/
foreach($response as $item) {
if ($item['type'] == 40) continue; // ignore announcements at this time
$sequence = $this->_extractSequenceFromEntry($item);
$duration = static::asHMS($item['out'] - $item['in']);
echo <<<BLOCK
<div>
<img src="data:image/jpeg;base64,{$item['thumb']}">
<p>{$sequence}. {$item['data']} {$item['speaker']} [{$duration}]</p>
</div>
BLOCK;
}
$ex = static::EXHORT;
//<div><a href="?page=talks-single&action=cygnus2&eid=$ex">Continue and add to HopeInStoughton</a></div>
echo <<<BLOCK
<div><a href="/~/media-add-cygnus">Continue and add to HopeInStoughton</a></div>
BLOCK;
}
private function _command_add_from_cygnus() {
$hash = $this->action_import_cygnus2(static::EXHORT);
// redirect to newly added entry
wp_redirect( admin_url( 'admin.php?page=talks-single&action=class&cid=' . $hash ) );
exit;
}
private function _extractSequenceFromEntry($item) {
list($yy, $mm, $dd) = explode('.', $item['date']);
$yy = intval($yy, 10); $mm = intval($mm, 10); $dd = intval($dd, 10);
$dt = gmmktime(12, 0, 0, $mm, $dd, $yy);
if (isset($item['type']) && $item['type'] == 100) {
$dt += 86400;
}
list($yy, $mm, $dd) = explode('.', gmdate('Y.n.j', $dt));
return $yy * 10000 + $mm * 100 + $dd;
}
protected function action_import_cygnus2($me_id) {
$curl = new WP_Http_Curl;
$args = [
'user-agent' => '',
'stream' => FALSE,
'filename' => '',
];
$response = $curl->request('https://cygnus.hopeinstoughton.org/~/videoEdit/current', $args);
$response = json_decode($response['body'], TRUE);
$hash = '';
foreach($response as $item) {
if ($item['type'] == 40) continue; // ignore announcements at this time
$sequence = $this->_extractSequenceFromEntry($item);
$speaker = $item['speaker']; // firstname lastname
$vitem = [
'SPEAKER' => $speaker,
'THUMBNAIL' => $item['thumb']
];
$thishash = $this->_preparev($sequence, $vitem, $item['date'], $item['type']);
if ($hash == '') $hash = $thishash;
}
return $hash;
}
private function _command_calendar_link($mi_id) {
global $wpdb, $MEDIASEARCH;
// determine correct url
$data = static::Load($mi_id, ['media' => FALSE]);
$link = '/video';
switch($data->mi_type) {
case 'ac':
$key = $data->mi_date;
$category = 3;
$link = '/' . $data->me_slug;
if ($data->mixe_sequence > 1) $link .= '/' . $data->mixe_sequence;
break;
case 'ex': // not for third exhort
$key = $data->mi_date;
$category = 2;
$item = ['date' => str_replace('-', '.', $key)];
$seq = $this->_extractSequenceFromEntry($item);
if ($seq != $data->mixe_sequence) {
return "Link generation not for devotional\n";
}
// password needed to access this content
$link = '/members/video-archive/?video-id=' . $data->mi_id;
break;
case 'bc':
$key = $data->mi_date;
$category = 4;
$link = '/' . $data->me_slug;
if ($data->mixe_sequence > 1) $link .= '/' . $data->mixe_sequence;
break;
default:
return "Link generation method unknown for type \"{$data->mi_type}\"\n";
}
// update calendar entry, if it exists
$tablename = $wpdb->get_blog_prefix().WP_SPIFFYCAL_TABLE;
$sql = $wpdb->prepare("update $tablename set event_link='%s' where event_begin='%s' and event_category=%d", $link, $key, $category);
$wpdb->get_results($sql);
return FALSE;
}
private function _command_generate_stats() {
$result = [];
$this->_generate_stats($result, 'Stoughton Exhortations', 'ex');
$this->_generate_stats($result, 'Stoughton Adult Classes', 'ac');
$this->_generate_stats($result, 'Stoughton Bible Classes', 'bc');
$this->_generate_stats($result, 'Moorestown', 'mt');
$this->_generate_stats($result, 'ChristadelphianBibleTalks', 'cbt');
$jsontext = json_encode($result, JSON_UNESCAPED_SLASHES);
var_export($jsontext);
update_option('media-manager-stats', $result, FALSE);
}
private function _generate_stats(&$result, $caption, $type) {
global $wpdb, $MEDIASEARCH;
$speakers = 0;
$talks = 0;
$duration = 0;
$sql = <<<'BLOCK'
select mi_speaker, count(mi_id) as talks, sum(m1.mm_duration) as duration
from `mm_item`
inner join mm_media as m1 on m1.mm_hash=mi_id and not m1.mm_duration is null
left join mm_media as m2 on m2.mm_hash=mi_id and not m2.mm_duration is null and m2.mm_source > m1.mm_source
where mi_type='%s' and m2.mm_hash is null
group by mi_speaker
BLOCK;
$sql = $wpdb->prepare($sql, $type);
$items = $wpdb->get_results($sql);
foreach($items as $item) {
$speakers ++;
$talks += (int)$item->talks;
$duration += (int)$item->duration;
}
$result[] = [
'type' => $type,
// 'source' => $caption,
'speakers' => $speakers,
'talks' => $talks,
'duration' => $duration,
];
}
/* crontab entry for user stoughton
15,45 * * * * curl -k -s -o /dev/null -v https://hopeinstoughton.org/~/media-cron > /dev/null 2>&1
*/
private function _command_cron() {
global $wpdb;
set_time_limit(0);
// get talks with YouTube url that does not have captions
$sql = <<<BLOCK
select mi_id, mi_other
from mm_item
where (present_mp3<>0 or present_mp4<>0)
and present_vtt=0
and mi_type <> "other"
and (mi_other like "%youtube.%" or mi_other like "%youtu.be%")
order by rand()
limit 5
BLOCK;
$items = $wpdb->get_results($sql);
foreach($items as $item) {
$date = date('Y-m-d H:i:s');
$payload = json_decode($item->mi_other, TRUE);
echo "$date: Processing {$payload['youtube']} [$item->mi_id]\n";
flush();
echo $this->_command_cron_youtube($item->mi_id, $payload['youtube']);
flush();
}
die("Done\n");
}
// do we have youtube closedcaptions for this talk?
private function _command_cron_youtube($mi_id, $youtube) {
global $wpdb, $MEDIASEARCH;
$url = $this->_getExternalMediaURL($mi_id);
if (is_null($url)) {
return "unknown talk identifier";
}
$youtube = $this->_getExternalMedia($url);
if ($youtube === FALSE) {
return "Original Url is missing or unrecognized";
}
if (!$youtube->hasCaption()) {
return "Captioning not yet present";
}
// download and save the captions, update database
$temp = new \StdClass;
$temp->content = $youtube->Caption();
$result = $this->_youtube_saveVtt($temp, $mi_id);
if (strpos($result, 'SUCCESS') !== FALSE) {
if (is_object($MEDIASEARCH)) {
$MEDIASEARCH->command_reindex($mi_id);
} else {
return "Unable to index captioning file";
}
} else {
return $result;
}
return '';
}
private function _command_archive_content($event_id) {
// get all the videos not on HOSTZ
$rows = $this->_command_datatables_classeslist($event_id, FALSE);
$target = 1 << static::HOSTZ;
foreach($rows as $row) {
$bitmap = (int) $row['present_mp4'];
$dt = date('Y-m-d h:i:s');
if (!$bitmap) {
echo "$dt Item {$row['mixe_sequence']} [{$row['mi_id']}] does not have video\n";
flush();
} elseif (!($bitmap & $target)) { // not on CDN2
echo "$dt Processing {$row['mixe_sequence']} [{$row['mi_id']}]\n";
flush();
// HOSTZ no longer exists so no need to copy the file
// $this->_archive_content_copyfile($row['mi_id'], $bitmap, static::HOSTZ);
if ($bitmap && (1 << static::ARIES)) {
$this->_archive_content_remove($row['mi_id'], static::ARIES); // remove from aries
}
} else {
echo "$dt Item {$row['mixe_sequence']} [{$row['mi_id']}] has been archived\n";
flush();
}
}
echo "\nDone\n";
exit;
}
private function _command_archive_content2($mi_id) {
global $wpdb;
// load the id
$did = '"' . esc_sql($mi_id) . '"';
$item = $wpdb->get_row("select mi_id,present_mp4,present_mp3 from mm_item where mi_id = $did");
$bitmap = (int) $item->present_mp4;
$bitmap3 = (int) $item->present_mp3;
$dt = date('Y-m-d h:i:s');
echo "$dt Processing {$item->mi_id}...\n";
flush();
if ($bitmap && (1 << static::ARIES)) {
$this->_archive_content_remove($item->mi_id, static::ARIES); // remove from aries
}
}
private function _command_media_clean() {
global $wpdb;
/*
$sql = "SELECT mi_id, mi_title FROM `mm_item` WHERE `mi_title` LIKE '%.mp3'";
$items = $wpdb->get_results($sql);
foreach($items as $item) {
$title = str_replace(['.mp3', '.MP3'], '', $item->mi_title);
echo "{$item->mi_id}. $title\n";
$did = '"' . esc_sql($item->mi_id) . '"';
$safetitle = '"' . esc_sql($title) . '"';
// echo "update mm_item set mi_title={$safetitle} where mi_id=$did\n";
$wpdb->Query("update mm_item set mi_title={$safetitle} where mi_id=$did");
}
$sql = "SELECT * FROM `mm_media` WHERE `mm_other` LIKE '%.mp3%'";
$items = $wpdb->get_results($sql);
foreach($items as $item) {
$source = $item->mm_source;
$hash = $item->mm_hash;
$payload = json_decode($item->mm_other, TRUE);
$payload['title'] = str_replace(['.mp3', '.MP3'], '', $payload['title']);
$safesource = '"' . esc_sql($source) . '"';
$safehash = '"' . esc_sql($hash) . '"';
$safepayload = '"' . esc_sql(str_replace('\\/', '/', json_encode($payload))) . '"';
$query = "update `mm_media` set mm_other={$safepayload} where mm_source={$safesource} and mm_hash={$safehash}";
$wpdb->Query($query);
}
$sql = "SELECT mi_id,mi_other FROM `mm_item` WHERE `mi_other` LIKE '%.mp3.mp3%'";
$items = $wpdb->get_results($sql);
foreach($items as $item) {
$hash = $item->mi_id;
$payload = json_decode($item->mi_other, TRUE);
$payload['original'] = str_replace(['.mp3.mp3', '.MP3.mp3', '.MP3.MP3'], '.mp3', $payload['original']);
$safehash = '"' . esc_sql($hash) . '"';
$safepayload = '"' . esc_sql(str_replace('\\/', '/', json_encode($payload))) . '"';
$query = "update `mm_item` set mi_other={$safepayload} where mi_id={$safehash}";
//echo "$query\n";
$wpdb->Query($query);
}
*/
}
private function _archive_content_copyfile($mi_id, $sources, $target) {
global $wpdb;
// get source folder/file
$src = $this->source->determineBestServer($sources, FALSE);
$filename_src = $this->source->getDestination($src, $mi_id . '.mp4');
$filename_target = $this->source->getDestination($target, $mi_id . '.mp4');
// copy the file - may take a while
copy($filename_src, $filename_target);
// add media record
$this->UpdateMediaTable($mi_id, $target, 'mp4');
// update flags
$mask = 1 << $target;
$did = '"' . esc_sql($mi_id) . '"';
$wpdb->Query("update mm_item set present_mp4 = present_mp4 | $mask where mi_id=$did");
}
private function _archive_content_remove($mi_id, $target) {
global $wpdb;
$filename_target = $this->source->getDestination($target, $mi_id . '.mp4');
@unlink($filename_target);
// remove media record
$did = '"' . esc_sql($mi_id) . '"';
$dsource = '"' . esc_sql($target) . '"';
$wpdb->Query("delete from mm_media where mm_hash=$did and mm_source=$dsource and mm_type=\"mp4\"");
// update flags
$mask = 1 << $target;
$wpdb->Query("update mm_item set present_mp4 = present_mp4 & ~ $mask where mi_id=$did");
}
// generate sql to resolve various problems
private function cdn_debug() {
/* // generate an authkey for test
$date = '2025.01.19';
$key = MediaSource::GenerateServiceAuth($date);
var_dump($key); */
global $wpdb;
$target = static::GOOGLE;
$mask = 1 << $target;
$sql = <<<BLOCK
select mi_id,present_mp4,mm_source from mm_item
left join mm_media on mm_hash=mi_id and mm_source = $target and mm_type='mp4'
where (present_mp4 <> 0) and (present_mp4 & $mask = 0) and not mm_type is null
BLOCK;
$sql = <<<BLOCK
select mi_id,present_mp3,mm_source from mm_item
left join mm_media on mm_hash=mi_id and mm_source = $target and mm_type='mp3'
where (present_mp3 <> 0) and (present_mp3 & $mask = 0) and not mm_type is null
BLOCK;
$items = $wpdb->get_results($sql);
echo "$sql\n\n";
var_dump($items);
return;
/* foreach($items as $item) {
$did = '"' . esc_sql($item->mi_id) . '"';
$wpdb->Query("update mm_item set present_mp4 = present_mp4 & ~ $mask where mi_id=$did");
} */
foreach($items as $item) {
$did = '"' . esc_sql($item->mi_id) . '"';
$wpdb->Query("update mm_item set present_mp3 = present_mp3 | $mask where mi_id=$did");
}
// everything that should be on 4.
}
// we have incorrect filesize stored in db
// file present at home, but no at-home entry: (1726 entries)
private function command_repair_mp3() {
global $wpdb;
$target = 4;
$mask = 1 << $target;
// mp3 present on target system - but missing in mm_media table
$sql = <<<BLOCK
select mi_id,present_mp3,mi_other
from mm_item as i
left join mm_media on mi_id=mm_hash and mm_source=$target and (mm_type='mp3' or mm_type='wma')
where (present_mp3 & $mask <> 0) and mm_hash is null
BLOCK;
$items = $wpdb->get_results($sql);
foreach($items as $index => $item) {
$item->mi_other = json_decode($item->mi_other, TRUE);
$this->_command_repair_mp3_check($item);
if ($index > 100) break;
}
$sql = <<<BLOCK
select mi_id,present_vtt,mi_other
from mm_item as i
left join mm_media on mi_id=mm_hash and mm_source=$target and (mm_type='vtt')
where (present_vtt & $mask <> 0) and mm_hash is null
BLOCK;
$items = $wpdb->get_results($sql);
foreach($items as $index => $item) {
$item->mi_other = json_decode($item->mi_other, TRUE);
$this->_command_repair_vtt($item);
if ($index > 100) break;
}
}
private function _command_repair_mp3_check($item) {
global $wpdb;
$id = $item->mi_id;
// get original filesize from cdn3
$dest = $this->source->getDestination(3, $id . '.mp3');
$filesize_orginal = filesize($dest);
if (!$filesize_orginal) {
echo "$dest - empty file\n";
flush();
return;
}
$filesize_current = (int) $item->mi_other['filesize'];
if ($filesize_orginal != $filesize_current) {
// update the filesize in the mm_item table
$item->mi_other['filesize'] = $filesize_orginal . '';
$did = '"' . esc_sql($id) . '"';
$safeother = '"' . esc_sql(json_encode($item->mi_other, JSON_UNESCAPED_SLASHES)) . '"';
$sql = "update mm_item set mi_other={$safeother} where mi_id=$did";
$wpdb->Query($sql);
echo "$dest - filesize adjusted from $filesize_current to $filesize_orginal\n";
flush();
}
$args = [
'user-agent' => '',
'stream' => FALSE,
'filename' => '',
];
$curl = new WP_Http_Curl;
$response = $curl->request('https://cdn3.hopeinstoughton.org/info?id=' . $id, $args);
$response = json_decode($response['body'], TRUE);
if (isset($response[$id]['mp3'])) {
// add the correct entry into mm_media table
// type, filesize duration, bitrate, mp3 tags
$info = $response[$id]['mp3'];
$filesize = $info['filesize'];
$duration = $info['duration'];
unset($info['filesize'], $info['duration'], $info['track']);
$types = ['%s', '%d', '%s', '%d', '%d', '%s'];
$values = [
'mm_hash' => $id,
'mm_source' => 4,
'mm_type' => 'mp3',
'mm_filesize' => $filesize,
'mm_duration' => $duration,
'mm_other' => json_encode($info, JSON_UNESCAPED_SLASHES),
];
$result = $wpdb->insert('mm_media', $values, $types);
if ($result) return 1;
echo "Unable to add entry for $id\n";
flush();
}
return 0;
}
private function _command_repair_vtt($item) {
global $wpdb;
$id = $item->mi_id;
$args = [
'user-agent' => '',
'stream' => FALSE,
'filename' => '',
];
$curl = new WP_Http_Curl;
$response = $curl->request('https://cdn3.hopeinstoughton.org/info?id=' . $id, $args);
$response = json_decode($response['body'], TRUE);
if (isset($response[$id]['vtt'])) {
$info = $response[$id]['vtt'];
$filesize = $info['filesize'];
$types = ['%s', '%d', '%s', '%d', '%d', '%s'];
$values = [
'mm_hash' => $id,
'mm_source' => static::HOME,
'mm_type' => 'vtt',
'mm_filesize' => $filesize,
'mm_other' => '[]',
];
$result = $wpdb->insert('mm_media', $values, $types);
if ($result) return 1;
echo "Unable to add entry for $id\n";
flush();
}
}
private function _command_google() {
global $wpdb;
$curl = new WP_Http_Curl;
$args = [
'user-agent' => '',
'stream' => FALSE,
'filename' => '',
];
$response = $curl->request('https://media.hopeinstoughton.org/~/mediaHiS/cdn-summary', $args);
$response = json_decode($response['body'], TRUE);
$types = ['%s', '%d', '%s', '%d', '%s'];
foreach($response as $hash => $ext) {
$list = explode(',', $ext);
foreach($list as $fileext) {
$values = [
'mm_hash' => $hash,
'mm_source' => static::GOOGLE,
'mm_type' => $fileext,
'mm_filesize' => 0,
'mm_other' => '[]',
];
$result = $wpdb->insert('mm_media', $values, $types);
}
}
die("Google Drive import done.\n");
}
public function UploadFile($filename, $type, $debug = FALSE) {
$keyfile = __DIR__ . '/cdn/.ht-www@mars.pem';
$safekey = escapeshellarg($keyfile);
$safefile = escapeshellarg($filename);
// if - Host key verification failed. - run the command from bash as user www-data - add key to the file
$command = "scp -i $safekey $safefile www-data@mars.hostz.org:/var/www/.incoming";
$res = `$command 2>&1`;
$xcommand = "scp $safefile www-data@mars.hostz.org:/var/www/.incoming";
if ($debug) echo "<pre>" . htmlspecialchars("$xcommand\n$res\n") . "</pre>";
// echo "<pre>" . htmlspecialchars("$command\n$res\n") . "</pre>";
$base = pathinfo($filename, PATHINFO_BASENAME);
$year = substr($base, 0, 4);
$single = $base[0];
switch($type) {
case 'sun':
$target = 'Memorial Service/' . $year;
break;
default:
$target = 'CDN/' . $single;
break;
}
$payload = [
'base' => $base,
'target' => $target, // target folder
];
$safebase = rawurlencode($base);
$safetarget = rawurlencode($target);
$command = "/usr/bin/curl -X POST -H \"Content-Type: application/x-www-form-urlencoded\" -d \"base={$safebase}&target={$safetarget}\" https://media.hopeinstoughton.org/~/mediahis/upload";
`$command > /dev/null 2&>1 &`;
}
// add the file to the whisper queue, as a priority event
private function action_whisper($mi_id) {
$url = 'https://hub.pjy.us/~/whisper/add-to-queue/' . $mi_id;
$curl = new WP_Http_Curl;
$args = [
'redirection' => 5,
'user-agent' => '',
'stream' => FALSE,
'filename' => '',
'decompress' => TRUE,
'_redirection' => 5,
];
$response = $curl->request($url, $args);
if (is_a($response, 'WP_Error')) {
header('Content-type: text/plain');
var_dump($url);
var_dump($response);
exit;
}
//
}
// export the files to googledrive - everything must be on aries first
private function action_googledrive($mi_id) {
global $wpdb;
// using scp, copy each of these to mars, and run curl to update the database.
// vtt, mp3, mp4
$safeid = esc_sql($mi_id);
// this is not great sql
$sql = <<<BLOCK
select mm_type, mm_source as ms, ms_url as mu, ms_path as mp
from `mm_media`
left join mm_source on mm_source=ms_id
where `mm_hash` = '$safeid'
group by mm_type;
BLOCK;
// and mm_type='vtt'
echo "<br><br><br>";
$items = $wpdb->get_results($sql);
$types = ['%s', '%d', '%s', '%d', '%s'];
foreach($items as $item) {
$base = $mi_id;
echo "Copying from {$item->mu}...<br>\n";
flush();
$filename = $item->mp . '/' . $base[0] . '/' . $base . '.' . $item->mm_type;
$this->UploadFile($filename, 'hash', TRUE); // copy to mars
$values = [
'mm_hash' => $base,
'mm_source' => static::GOOGLE,
'mm_type' => $item->mm_type,
'mm_filesize' => 0,
'mm_other' => '[]',
];
$result = $wpdb->insert('mm_media', $values, $types);
$did = '"' . esc_sql($mi_id) . '"';
$mask = 1 << static::GOOGLE;
if ($item->mm_type == 'mp3') {
$wpdb->Query("update mm_item set present_mp3 = present_mp3 | $mask where mi_id=$did");
} elseif ($item->mm_type == 'mp4') {
$wpdb->Query("update mm_item set present_mp4 = present_mp4 | $mask where mi_id=$did");
} elseif ($item->mm_type == 'vtt') {
$wpdb->Query("update mm_item set present_vtt = present_vtt | $mask where mi_id=$did");
} elseif ($item->mm_type == 'jpg') {
$wpdb->Query("update mm_item set present_jpg = present_jpg | $mask where mi_id=$did");
}
}
$deet = admin_url( 'admin.php?page=talks-single&action=class&cid=' . $mi_id );
echo "<p><a href=\"{$deet}\">Return</a> to Talk Detail page</p>";
//echo "<br><pre>" . htmlspecialchars(var_export($items, 1)) . "</pre>";
}
private function _repairVttDistribution($row) {
$media = static::Load($row['mi_id'], ['media' => TRUE]);
// load the Aries vtt
$source = $this->source->getDestination(static::ARIES, $row['mi_id'] . '.vtt');
$content = file_get_contents($source);
$data = new \StdClass;
$data->mi_id = $row['mi_id'];
foreach($media->_media as $row) {
if ($row->mm_type == 'mp3' && ($row->mm_source == static::CHURCH || $row->mm_source == static::GOOGLE)) {
switch($row->mm_source) {
case static::CHURCH:
$this->_ProcessVTTfile_upload(static::CHURCH, $data, $content); // save to stoughton
break;
case static::GOOGLE:
$this->_ProcessVTTfile_upload(static::GOOGLE, $data, ''); // copy to GoogleDrive - must be done after Aries copy (but aries version is up-to-date here
break;
}
}
}
}
public function repairVttDistribution() {
set_time_limit(0);
$mm_media = [
['mi_id' => 'f7f595d6','mc_filesize' => '368825','ma_filesize' => '232278'],
['mi_id' => 'f82620ac','mc_filesize' => '345877','ma_filesize' => '226681'],
['mi_id' => 'f85bb7fa','mc_filesize' => '284553','ma_filesize' => '181157'],
['mi_id' => 'f8e3b77f','mc_filesize' => '506964','ma_filesize' => '309198'],
['mi_id' => 'f98ab968','mc_filesize' => '468291','ma_filesize' => '306204'],
['mi_id' => 'f9e0adb5','mc_filesize' => '515587','ma_filesize' => '338458'],
['mi_id' => 'f9f2d70f','mc_filesize' => '354701','ma_filesize' => '231657'],
['mi_id' => 'fa1ca367','mc_filesize' => '296426','ma_filesize' => '184372'],
['mi_id' => 'fa724f8b','mc_filesize' => '515587','ma_filesize' => '338458'],
['mi_id' => 'fa88a7fc','mc_filesize' => '434148','ma_filesize' => '290179'],
['mi_id' => 'fb328507','mc_filesize' => '358821','ma_filesize' => '223725'],
['mi_id' => 'fb8ed4a7','mc_filesize' => '261611','ma_filesize' => '162362'],
['mi_id' => 'fe22c8a8','mc_filesize' => '319672','ma_filesize' => '202298'],
['mi_id' => 'fe6cae3a','mc_filesize' => '357636','ma_filesize' => '226096'],
['mi_id' => 'fea7111f','mc_filesize' => '435615','ma_filesize' => '282328'],
['mi_id' => 'ff8246fb','mc_filesize' => '484244','ma_filesize' => '322572'],
['mi_id' => 'fff9ee7f','mc_filesize' => '324585','ma_filesize' => '212326'],
['mi_id' => 'f9d87c29','mc_filesize' => '279009','ma_filesize' => '138887'],
['mi_id' => '4b448733','mc_filesize' => '220106','ma_filesize' => '110183'],
['mi_id' => 'd053f808','mc_filesize' => '240971','ma_filesize' => '122368'],
['mi_id' => '91668544','mc_filesize' => '221392','ma_filesize' => '111808'],
['mi_id' => 'c69e6bd5','mc_filesize' => '284771','ma_filesize' => '144195'],
['mi_id' => '50fa9b8f','mc_filesize' => '267209','ma_filesize' => '137071'],
['mi_id' => '73cde7ca','mc_filesize' => '259913','ma_filesize' => '159897'],
['mi_id' => '6b2e4b5c','mc_filesize' => '348170','ma_filesize' => '224987'],
['mi_id' => '84a96260','mc_filesize' => '295511','ma_filesize' => '185222'],
['mi_id' => 'b2f9150a','mc_filesize' => '0','ma_filesize' => '98689'],
['mi_id' => '7a29def1','mc_filesize' => '90284','ma_filesize' => '90333']
];
foreach($mm_media as $row) {
if ($row['ma_filesize'] < $row['mc_filesize']) {
echo "{$row['mi_id']}\n";
flush();
$this->_repairVttDistribution($row);
}
}
die("\nDone\n");
}
}
/*
CREATE TABLE `mm_source` (
`ms_id` int(10) UNSIGNED NOT NULL,
`ms_name` varchar(80) NOT NULL COMMENT 'name of location',
`ms_url` varchar(255) DEFAULT NULL COMMENT 'external path',
`ms_path` varchar(512) DEFAULT NULL COMMENT 'internal path',
`ms_mountfile` varchar(32) DEFAULT NULL COMMENT 'the name of a file indicating mount established'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='description of server hosting source files';
INSERT INTO `mm_source` (`ms_id`, `ms_name`, `ms_url`, `ms_path`, `ms_mountfile`) VALUES
(1, 'Aries', 'https://cdn1.hopeinstoughton.org', '/var/www/hopeinstoughton_cdn1', '.htaccess'),
(2, 'Hostz', 'https://media.hopeinstoughton.org', '/mnt/media', 'index.html'),
(3, 'Church', 'https://cdn3.hopeinstoughton.org', '/mnt/stoughton', '.htaccess'),
(4, 'Home', 'https://cdn4.hopeinstoughton.org', '/mnt/home', '.mounted');
ALTER TABLE `mm_source`
ADD PRIMARY KEY (`ms_id`);
ALTER TABLE `mm_source`
MODIFY `ms_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
CREATE TABLE `mm_speaker` (
`ms_id` int(10) UNSIGNED NOT NULL,
`ms_firstname` varchar(100) DEFAULT NULL,
`ms_lastname` varchar(100) DEFAULT NULL,
`ms_suffix` varchar(10) DEFAULT NULL COMMENT 'jr or sr, etc',
`ms_ecclesia` varchar(60) DEFAULT NULL,
`ms_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'alive?'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `mm_speaker`
ADD PRIMARY KEY (`ms_id`),
ADD KEY `ms_lastname` (`ms_lastname`,`ms_firstname`,`ms_suffix`),
ADD KEY `ms_ecclesia` (`ms_ecclesia`);
ALTER TABLE `mm_speaker`
MODIFY `ms_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
CREATE TABLE `mm_event` (
`me_id` int(10) UNSIGNED NOT NULL,
`me_location` varchar(128) DEFAULT NULL,
`me_slug` varchar(128) DEFAULT NULL,
`me_topic` varchar(128) DEFAULT NULL,
`me_year` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='a collection of talks by a speaker';
ALTER TABLE `mm_event`
ADD PRIMARY KEY (`me_id`),
ADD KEY `me_location` (`me_location`(80),`me_topic`(80),`me_year`),
ADD KEY `me_slug` (`me_slug`) USING BTREE,
ADD KEY `me_topic` (`me_topic`) USING BTREE;
ALTER TABLE `mm_event`
MODIFY `me_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
CREATE TABLE `mm_item` (
`mi_id` varchar(10) NOT NULL,
`mi_date` date DEFAULT NULL COMMENT 'null if unknown',
`mi_title` varchar(512) DEFAULT NULL,
`mi_speaker` int(10) DEFAULT NULL,
`mi_type` enum('unknown','ex','mt','ac','bc','bapt','study','cyc','cbt') DEFAULT NULL,
`mi_thumbnail` mediumtext null default null,
`mi_moorestown` int(11) DEFAULT NULL,
`mi_duration` int(11) DEFAULT NULL,
`mi_other` mediumtext COMMENT 'json: filename, filesize, filehash, framesize',
`present_mp4` int(11) NOT NULL DEFAULT '0' COMMENT 'location of video file. bitmap',
`present_mp3` int(11) NOT NULL DEFAULT '0' COMMENT 'location of audio file. bitmap',
`present_jpg` int(11) NOT NULL DEFAULT '0' COMMENT 'location of poster',
`present_vtt` int(11) NOT NULL DEFAULT '0' COMMENT 'location of captions',
`present_xml` INT(11) NOT NULL DEFAULT '0' COMMENT 'location of preparev xml'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='media items';
ALTER TABLE `mm_item`
ADD PRIMARY KEY (`mi_id`),
ADD KEY `mi_date` (`mi_date`),
ADD KEY `mi_moorestown` (`mi_moorestown`),
ADD KEY `mi_type` (`mi_type`,`mi_date`);
CREATE TABLE `mm_item_x_event` (
`mixe_event` int(10) UNSIGNED NOT NULL,
`mixe_sequence` int(11) NOT NULL,
`mixe_item` varchar(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='items included in an event';
ALTER TABLE `mm_item_x_event`
ADD PRIMARY KEY (`mixe_event`,`mixe_sequence`,`mixe_item`),
ADD KEY `mixe_item` (`mixe_item`);
COMMIT;
// http://archive.moorestownchristadelphians.org/index.php?option=com_biblestudy&view=landingpage&Itemid=9
// download: /index.php?option=com_biblestudy&id=26&view=studieslist&controller=studieslist&task=download&Itemid=3
// http://archive.moorestownchristadelphians.org/downloads/
// chrome does not like media links to http:// from https pages and will automatically rewrite them
SELECT * FROM `mm_item` where present_mp3 <> 0 and present_vtt = 0 ORDER BY `mi_id` ASC
At home, but no at-home entry: (1726 entries)
select i.*
from mm_item as i
left join mm_media on mi_id=mm_hash and mm_source=4 and (mm_type='mp3' or mm_type='wma')
where (present_mp3 & 16 <> 0) and mm_hash is null
*/
/*
ffmpeg -loop 1 -i './include/background-blu.jpg' -i './5/5c9dc850.mp3' -c:a copy -c:v libx264 -shortest /tmp/mp4U5eTkB.mp4
<div id="upload_area" style="max-width:1200px">
<div id="dzfile_2" class="mm-dropzone dropzone-frame dz-clickable" href="/~/media-fileupload/{$mi_id}">
<div class="dz-message needsclick">
Drop file here or click to upload.
</div>
</div>
</div>
// Get a list of all audio
select me_location, me_year, ms_firstname, ms_lastname, ms_suffix, me_topic, mixe_sequence, mi_id, mi_date, mi_title, mi_type
from mm_item_x_event
left join mm_item on mixe_item=mi_id
left join mm_speaker on mi_speaker=ms_id
left join mm_event on me_id = mixe_event
where present_mp3 <> 0 and mixe_event <> 364
order by mixe_event, mixe_sequence
vtt reprocessing errors (not copied to stoughton or google):
select mi.mi_id,
mc.mm_filesize as mc_filesize,
ma.mm_filesize as ma_filesize
from mm_item as mi
inner join mm_media as mc on mc.mm_hash=mi_id and mc.mm_source=3 and mc.mm_type=3
inner join mm_media as ma on ma.mm_hash=mi_id and ma.mm_source=1 and ma.mm_type=3
where mc.mm_filesize <> ma.mm_filesize
*/
new media_manager_class();