Node, Puppeteer: автоматизация Google Chrome. Сохраняем ts файлы и преобразуем в mp4.

Используем именно Google Chrome, так как в Chromium не все работает, например, Shaka player (Google).
После установки Node.js
sudo dnf module list nodejs
sudo dnf module enable nodejs:12
sudo dnf install nodejs
Устанавливаем puppeteer без Chromium (350 МВ)
echo "export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true && npm install i puppeteer" | sudo sh
При запуске puppeteer указываем путь к исполняемому файлу Google Chrome
 executablePath: '/opt/google/chrome/google-chrome',
Выключаем CORS
        '--disable-web-security',
        '--disable-site-isolation-trials',
Из интересного для сохранения «.m3u8» и «.ts» используем page.evaluate, далее fetch в контексте браузера, затем передаем содержимое файла в puppeteer
let dataString = '[' + (new Uint8Array(data)).toString() + ']';
и сохраняем файл определенной с помощью page.exposeFunction функцией writefile.
'use strict';
const puppeteer = require('puppeteer');
const fs = require('fs');

(async function main() {
  var browser;
  try {

    var myArgs = process.argv.slice(2);
    if (!myArgs.length) {
        throw('need dir');        
    }
    console.log('dir: ', myArgs[0]);


    browser = await puppeteer.launch({
      headless: true,
      slowMo: 250, // slow down by 250ms
//      devtools: true,
      executablePath: '/opt/google/chrome/google-chrome',
      defaultViewport: null, // иначе не работает --window-size=1920,1080
      args:[
        '--window-size=1920,1080',
        '--disable-setuid-sandbox',
	'--no-sandbox',
        '--disable-infobars',
        '--disable-web-security',
        '--disable-site-isolation-trials',
        ],
    });

/*
    var lessonUrl = myArgs[0]

    const crypto = require('crypto');
    const hash = crypto.createHash('sha256');
    hash.update(lessonUrl);

    var dir = 'fdb' + '/' + hash.digest('hex');
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir);
    }
    var dirOriginal = dir + '/' + 'original';

    if (!fs.existsSync(dirOriginal)) {
        fs.mkdirSync(dirOriginal);
    }
*/

    var lessonUrl = myArgs[1];
    var dir = myArgs[0];
    var dirOriginal = dir + '/' + 'original';


    const regUrl = 'https://zzz.ru/user/registration'
    const page = await browser.newPage()
    page.on('console', msg => console.log('PAGE LOG:', msg.text()));
//    await page.setDefaultNavigationTimeout(60000);
    await page.goto(regUrl, { waitUntil: 'load' })
    await page.waitForTimeout(1000);
    await page.screenshot({ path: dir + '/' + 'screenshot01.png' })

    let checkUrl = await page.evaluate(() => location.href);
    if (regUrl == checkUrl) {
        await page.click('[data-qa="__authEmailButton"]')
        await page.type('#email', 'username@mail.ru')
        await page.type('#password', '12wdnj4ss7ty')
        await page.screenshot({ path: dir + '/' + 'screenshot02.png' })

        await Promise.all([
          page.click('[data-qa="__authEmailSubmitButton"]'),
          page.waitForNavigation(), // if "await page.waitForNavigation" then "TimeoutError: Navigation timeout of 30000 ms exceeded", причем на ноде поновее всё работало
        ]);
    }
    await page.waitForTimeout(2000);
    await page.screenshot({ path: dir + '/' + 'screenshot1.png' })
    console.log(lessonUrl);

    await page.goto(lessonUrl, { waitUntil: 'load' })
    await page.waitForTimeout(2000);
    await page.screenshot({ path: dir + '/' + 'screenshot2.png' })
/*
// forEach и async не очень дружат
    var videoFrame;
    page.frames().forEach(async (frame) => {
        let tt = await frame.$('#shaka-video');
        if (tt) {
            videoFrame = frame;
        }
    });
*/

    var videoFrame;
    for (const frame of page.frames()) {
        let tt = await frame.$('#shaka-video');
        if (tt) {
            videoFrame = frame;
        }
    }
    if (!videoFrame) {
            throw('#shaka-video not found!');
    }
    if (videoFrame) {
        console.log(videoFrame.url());
        await page.goto(videoFrame.url(), { waitUntil: 'load' })
        await page.waitForTimeout(5000);
        await page.screenshot({ path: dir + '/' + 'screenshot3.png' })
        const filesUrls = await page.evaluate(() => {
            var urls = [];
            var ui0 = document.getElementById('shaka-player').ui;
            urls.push(ui0.b.Fa);
            // "https://storage.svc.services/api/v2/backends/yandex/sets/hls.webinar::198a2db0-e74b-44c8-ac22-7cec61972e8a/objects/long.v2.yandex.master.m3u8"
            // ссылка после редиректа получаем подписанные (pre-signed) URL
            // ui0.b.s.K
            // "https://storage-lb.services/hls.webinar/198a2db0-e74b-44c8-ac22-7cec61972e8a.long.v2.yandex.master.m3u8?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=DNmtuhzj5w4VFKS-9BUA%2F20201228%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20201228T142459Z&X-Amz-Expires=300&X-Amz-Signature=0a10db545d16abdfd81c988d7f6e488dfc36502aab60456da78afa9f1ed5bac8&X-Amz-SignedHeaders=host"
            var a0=v0=undefined;
            // select audio and video streams
            ui0.b.s.a.forEach((v,k)=>{ 
                if (!a0 && v.stream.type=='audio') {a0=v;} 
                if (v.stream.type=='video' && (!v0 || v.stream.width > v0.stream.width)) {v0=v;} 
            });
            // load and save selected audio and video segments description files
            urls.push(a0.Wc);
            urls.push(v0.Wc);
//      ["https://storage.svc.services/api/v2/backends/yandex/sets/hls.webinar::198a2db0-e74b-44c8-ac22-7cec61972e8a/objects/long.audio.32kbps.1.1608056896.ts" length: 1 __proto__: Array(0)

            a0.stream.segmentIndex.a.forEach((v,k)=>{ 
                urls = urls.concat(v.c());
            });
            v0.stream.segmentIndex.a.forEach((v,k)=>{ 
                urls = urls.concat(v.c());
            });

            return urls;
        });        
        console.log(filesUrls[0], filesUrls[1], filesUrls[2], filesUrls[3], filesUrls.length);

        var filesUrlsSaved = 0;

        await page.exposeFunction('writefile', async (filePath, data) => {
//          let dataBuffer = new TextEncoder().encode(data);
          filesUrlsSaved++;
          let dataBuffer = new Uint8Array(JSON.parse(data));

          return new Promise((resolve, reject) => {
            fs.writeFile(filePath, dataBuffer, {encoding: null}, (err) => {
              if (err)
                reject(err);
              else
                resolve();
            });
          });
        });

        if (filesUrls.length) {
            fs.writeFileSync(dirOriginal + '/loading', filesUrls[1] + '\n' + filesUrls[2] + '\n', {flag: 'w'});
        }

        for (var loadFile of filesUrls) {
            await page.evaluate(async (dirOriginal, loadFile) => {
                let fileName = loadFile.match('[^/]*$')[0].match('^[^#?]*');
                await fetch(loadFile, {
                    method: "GET",
                    redirect: 'follow',
                }).then((response) => {
                    console.log('url with auth', response.url);
                    return response.arrayBuffer();
                }).then(async function(data) {
//                    let dataView = new Uint8Array(data);
//                    let dataString = new TextDecoder().decode(data);
                    let dataString = '[' + (new Uint8Array(data)).toString() + ']';
                    await window.writefile(dirOriginal + '/' + fileName, dataString);
                });
            }, dirOriginal, loadFile);

/*
            // error 403
           var viewSource = await page.goto(loadFile);
           fs.writeFile(dirOriginal + '/' + fileName, await viewSource.buffer(), function (err) {
               if (err) {
                   return console.log(err);
               }
           });
*/
        }

//        if (filesUrls.length) {
//            fs.writeFileSync(dirOriginal + '/loaded', '', {flag: 'w'});
//        }

        if (filesUrls.length != filesUrlsSaved) {
            console.error('filesUrls:', filesUrls.length, filesUrlsSaved);
            throw('Not all files saved!');
        }

    }

    browser.close()
    console.log('See screenshot in: ' + dir)
  } catch (err) {
    console.error(err);
    browser.close()
  }
})();
Еще чуток бэкенда на php
<?php
...
    // выполнение внешней программы и получение stdout, stderr 
    function exec_return($cmd, $input = '') {
        $descriptorspec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
        $proc = proc_open($cmd . "\n", $descriptorspec, $pipes);
        foreach ($pipes as $pipe) {
            stream_set_blocking($pipe, false);
        }
        fwrite($pipes[0], $input); fclose($pipes[0]);
//        $stdout = stream_get_contents($pipes[1]); 
//        $stderr = stream_get_contents($pipes[2]); 
        $stdout = ''; 
        $stderr = ''; 
        while (!feof($pipes[1])) $stdout .= fgets($pipes[1],1024);
        while (!feof($pipes[2])) $stderr .= fgets($pipes[2],1024);
        fclose($pipes[1]);
        fclose($pipes[2]);
        $return = proc_close($proc);
        return [
            'stdout' => $stdout,
            'stderr' => $stderr,
            'return' => $return
        ];
    }
...
                    // копируем все ts в один файл output.mp4 без преобразования, очень быстро
                    // при склейке обычно "убегает" звук, "-itsoffset 0.5", сдвигает видео дорожку на 500 мс
                    $command = sprintf('/usr/bin/ffmpeg -loglevel repeat+level+error -nostats -y -i "%s" -itsoffset 0.147978 -i "%s" -vcodec copy -c:a copy "output.mp4"', $m3u8files[0], $m3u8files[1]);
                    $ret = exec_return($command);
                    file_put_contents("{$forConvert->full}/converted/error", $ret['stderr']);
                    chmod("{$forConvert->full}/converted/error", 0666);                        
                    file_put_contents("{$forConvert->full}/converted/converted", $ret['stdout']);
                    chmod("{$forConvert->full}/converted/converted", 0666);

...

PHP extracting zip archives with cyrillic filenames problem

Наткнулись на проблему, при распаковке zip архива (php7), если в именах русские символы получаем каракули (в нашем случае только они и присутствуют), странным способом перекодированные. Создаем архив используя ДОСовскую кодировку 866 с ней нет проблем со встроенными в винду зип-архиватором, если использовать 1251 в винде до 8-ки опять же каракули.

	$zip->addFile($pathFrom . '/' . $doc->hashedName,  mb_convert_encoding($doc->originalName, 'CP866', 'UTF-8'));
Пытаемся распаковать из ПХП

$Zip = new ZipArchive();
$open = $Zip->open('in_01122016000000.zip');
$length = $Zip->numFiles;
for($i = 0; $i < $length; $i++)
{
  echo $Zip->getNameIndex($i) . ' ' . mb_convert_encoding($Zip->getNameIndex($i), 'UTF-8', 'UTF-8, CP866') . '<br>';
}
Заглянув в исходники zip расширения, находим, что начиная с php 5.6 при получении имени файла вызывается _zip_string_get, где используется логика угадывания кодировки (/* start guessing */) и для получения не измененной строку нужно указать флаг ZIP_FL_ENC_RAW. Пока в официальной документации о такой возможности умалчивают, речь только о флаге ZipArchive::FL_UNCHANGED — до изменений.

  //zip.h
  //#define ZIP_FL_ENC_RAW         64u /* get unmodified string */
  echo mb_convert_encoding($Zip->getNameIndex($i, 64), 'UTF-8', 'UTF-8, CP866') . '<br>';
или так

  $stat = $Zip->statIndex($i, 64);
  echo mb_convert_encoding($stat['name'], 'UTF-8', 'UTF-8, CP866') . '<br>';
ZipArchive reads filenames with UTF-8 characters wrong

Phalcon: calling getRender() multiple times

При вызове метода getRender() несколько раз подряд, данные будут только при первом вызове.

$html1 = $view->getRender($section, $template, $data);
$html2 = $view->getRender($section, $template, $data);

var_dump($html1); // Valid string
var_dump($html2); // Empty string
Дабы исправить ситуацию, регистрируем сервис вольта немного иначе

	$di->set('view', function() use ($config) {

		$view = new \Phalcon\Mvc\View();

		$view->setViewsDir(__DIR__ . $config->application->viewsDir);
// было
//		$view->registerEngines(array(
//			".volt" => 'volt'
//		));

// стало
		$view->registerEngines(array(
		        ".volt" => function($view, $di) {
				$volt = new \Phalcon\Mvc\View\Engine\Volt($view, $di);
					$volt->setOptions(array(
						"compiledPath" => "../cache/volt/",
					));
				return $volt;
		        }
		));


		return $view;
	});

Skipping recall function from a script php

Пропуск повторного вызова функции из скрипта php.
session_id('cron'); //используем именованную сессию, имя файла сессии sess_cron
session_start();
if ($this->session->has(__METHOD__)) {
	$attemptCount = intval($this->session->get('attemptCount-' . __METHOD__));
	$this->session->set('attemptCount-' . __METHOD__, ++$attemptCount);
	printf("Run at: %s\r\n", date('c', intval($this->session->get(__METHOD__))));
	return;
} else {
	$this->session->set(__METHOD__, $_SERVER["REQUEST_TIME_FLOAT"]);
	$this->session->set('attemptCount-' . __METHOD__, 1);
	session_write_close(); // закроем сессию, снимем с нее блокировку иначе следующие скрипты не смогут ничего прочитать и будут ждать в очереди, а нам нужен другой эффект
}

sleep(300); //тут что-то делаем

//снимаем блокировку
session_id('cron');
session_start();
$this->session->remove(__METHOD__);
Безымянный91
если хотим аналог мьютекса, то достаточно только session_id(‘cron’) и все, все скрипты в очереди на ожидание ресурса — файла сессии

php анонимная функция с возможностью немедленного выполнения

Аналог анонимной функции JavaScript с немедленным исполнением
(function () { /* do something */ })()

модель рабочего примера, найти первое вхождение, частичное или полное

#php 5.3
$ttttar212 = ['asf',['asd','qwer2'],'qwe'];
$inttttar = 0;
call_user_func(function($cont_act, $ttttar212) use (&$inttttar) {
	foreach ($ttttar212 as $v) {
		if ($cont_act[0] === $v || $cont_act === $v) {
			$inttttar++;
			break;
		}
	}
}, ['qwe','qwer'], $ttttar212);

в действии

// если совпадает с внутренними (с виртуальными) то тоже подсветим
if ($data->active == 0 && isset($data->virtual) && is_array($data->virtual)) {
	call_user_func(function($cont_act, $ttttar) use (&$data) {
		foreach ($ttttar as $v) {
			if ($cont_act[0] === $v || $cont_act === $v) {
				$data->active++;
				break;
			}
		}
	}, [$controllerName, $actionName], $data->virtual);
//	$data->active += (int)in_array($controllerName, $data->virtual);
//	$data->active += (int)in_array([$controllerName,$actionName], $data->virtual);
}

Сессии php и десериализация

Задача, проход по списку сессий, сессия стандартно в файлах, получить доступ к данным.
<div id="usersmonitor" >
<table id="usersmonitortable" class="table table-bordered table-hover table-condensed table-fixed">
<thead>
<tr><th style="width: 15%;">Последняя активность</th><th style="width: 35%;">ФИО</th><th style="width: 25%;">e-mail</th><th style="width: 25%;">Роль</th></tr>
</thead>
<tbody>
<?php 
function unserialize_php($session_data) {
        $return_data = array();
        $offset = 0;
        while ($offset < strlen($session_data)) {
            if (!strstr(substr($session_data, $offset), "|")) {
                throw new Exception("invalid data, remaining: " . substr($session_data, $offset));
            }
            $pos = strpos($session_data, "|", $offset);
            $num = $pos - $offset;
            $varname = substr($session_data, $offset, $num);
            $offset += $num + 1;
            $data = unserialize(substr($session_data, $offset));
            $return_data[$varname] = $data;
            $offset += strlen(serialize($data));
        }
	return $return_data;
}

$directory = session_save_path().'/';
$filenames = array();
$iterator = new DirectoryIterator($directory);
foreach ($iterator as $fileinfo) {
    if ($fileinfo->isFile()) {
        $filenames[$fileinfo->getMTime()] = $fileinfo->getFilename();
    }
}
krsort($filenames);

foreach($filenames as $mt => $fn) {
 $ot = new \DateTime();
 $ot->setTimestamp($mt);
 $st = $ot->format('d.m.Y H:i');
 $t = unserialize_php(file_get_contents($directory . $fn));
 if (isset($t['auth']) && isset($t['auth']['user'])) {
	$tt = (object)$t['auth']['user'];
	echo "<tr><td>{$st}</td><td>{$tt->nameLast} {$tt->nameFirst} {$tt->nameMidle}</td><td>{$tt->email}</td><td>{$tt->name}</td></tr>";
 }
}

?>
</tbody>
</table>
</div>

<script>
$(function () {
	setInterval( function() {$('#usersmonitor').load('/users/usersmonitor #usersmonitortable', {act: 'refresh'})} , 30100 );
});
</script>

Авторизация учетными данными JIRA в PHP

В этом конкретном случае «прикручено» в Yii. Читаем непосредственно с таблицы пользователей Jira
<?php

namespace app\models;

class User extends \yii\base\Object implements \yii\web\IdentityInterface
{
    public $id;
    public $username;
    public $password;
    public $authKey;
    public $accessToken;

    public $credential;
    public $first_name;
    public $last_name;
    public $gid;
    public $group_name;


    public static function _init() {
	if (empty(self::$users)) {
		$users = \Yii::$app->db->createCommand("select c.id, c.lower_user_name username, c.credential, c.first_name, c.last_name from cwd_user c where c.active = 1")
           			->cache(3600)
           			->queryAll();
           	foreach ($users as $u) {
			self::$users[$u['id']] = $u;
		}
	}

    }

    public static $users = [];
...............................
    /**
     * Validates password
     *
     * @param  string  $password password to validate
     * @return boolean if password provided is valid for current user
     */
    public function validatePassword($password)
    {
	return JiraAuth::isValidPassword($password, $this->credential);
    }
} 

И сам класс проверки пароля
<?php

class JiraAuth {
	const DEFAULT_PREFIX = '{PKCS5S2}';
	const DEFAULT_SALT_LENGTH_BYTES = 16;

	public static function isValidPassword($password, $credential) {
		$ret = false;

		if ( strrpos($credential, JiraAuth::DEFAULT_PREFIX) === false)
			return $ret;

		$credential = substr($credential, strlen(JiraAuth::DEFAULT_PREFIX)-1);

		$binSaltAndKey = base64_decode($credential);
		
		if (strlen($binSaltAndKey)<48)
			return $ret;

		$salt = substr($binSaltAndKey,0,JiraAuth::DEFAULT_SALT_LENGTH_BYTES);
		$oldKey = bin2hex(substr($binSaltAndKey,JiraAuth::DEFAULT_SALT_LENGTH_BYTES));
		$newKey = hash_pbkdf2('sha1', $password, $salt, 10000, 64);

		return ($oldKey == $newKey);
	}
}
Замечание, PHP нужен от 5.5, а JIRA была версии v6.3

Загрузка файлов, параметр filename в HTTP заголовке Content-Disposition

«Крякозябры» вместо имени файла в IE или ошибка загрузки и сообщение о дублирующемся заголовке на движке WebKit.
if (file_exists($path . '/' . $dfi->hashedName)) {
	$response = new \Http\Response();
	$response->setHeader("X-UA-Compatible", 'IE=edge');
	$response->setHeader("Content-Type", isset($dfi->mimeType) && !$nomime  ? $dfi->mimeType : 'application/octet-stream');
	$response->setHeader("Content-Transfer-Encoding", 'binary');
	$response->setHeader("Content-Disposition", ($nomime ? 'attachment' : 'inline') . '; filename=' . str_replace(['\\','"','#','*',';',':','|','<','>','/','?',','], '_', mb_convert_encoding($dfi->originalName, 'CP1251', 'UTF-8')) . '; filename*=UTF-8\'\''.rawurlencode($dfi->originalName).'');

	$response->setHeader("Content-Length", (string)(filesize($fpath)));
	$response->sendHeaders();
	if ($file = fopen($fpath, 'rb')) {
		while(!feof($file) and (connection_status()==0 || 1)) {
			print(fread($file, 1024*8));
			flush();
		}
		fclose($file);
	}
	exit;
}

Использование переменной окружения .htaccess

cat ./.htaccess
SetEnv APPLICATION_ENV production
AddDefaultCharset UTF-8
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?_url=/$1 [QSA,L]
Используем в php (по умолчанию будет использоваться development):
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV',
              (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV')
                                         : 'development'));
Вся затея ради того, чтобы использую настройки окружения хоста переключаться между БД разработки и продакшн
public $db_our = '[Metrolog' . (APPLICATION_ENV!='production' ? 'Dev' : '') . '].[dbo].';
И сразу, защитим .htaccess от нечаянного изменений даже в случае наличия рутовых прав, выставим атрибут файлу IMMUTABLE(Неизменяемый) (это для Linux)
# lsattr ./.htaccess
-------------e-- ./.htaccess
# chattr +i ./.htaccess
# lsattr ./.htaccess
----i--------e-- ./.htaccess
(a это для UNIX)
# ls -lo ./.htaccess
-rw-r--r--  1 rianvi  wheel  - 375 Feb 11  2013 ./.htaccess
# chflags schg ./.htaccess
# ls -lo ./.htaccess
-rw-r--r--  1 rianvi  wheel  schg 375 Feb 11  2013 ./.htaccess