diff($dFim);
$days = $dDiff->days;
$time = $dStart->format('H:i');
if($days <= 0) {
return __('general.date_today', ['time' => $time]);
} else if($days == 1) {
return __('general.date_yesterday', ['time' => $time]);
}
return __('general.date_full', [
'date' => $dStart->format(__('general.date_format')),
'time' => $time,
]);
}
}
if(!function_exists('limitName')) {
function limitName($name){
if(strlen($name) > 15){
$expName = explode(' ',$name);
return $expName[0];
}
return $name;
}
}
if(!function_exists('isLight')) {
function isLight($color){
$hexRGB = str_replace('#','',$color);
if(hexdec(substr($hexRGB,0,2))+hexdec(substr($hexRGB,2,2))+hexdec(substr($hexRGB,4,2))> 381){
return 'lighter';
}
}
}
if(!function_exists('EncryptBras')) {
function EncryptBras($string,$type = 'e'){
$key = 'ABRASXCASTENC9X9ETODEC';
if($type != 'e'){
$encryption_key = base64_decode($key);
list($encrypted_data, $iv) = explode('::', base64_decode($string), 2);
return openssl_decrypt($encrypted_data, 'aes-256-cbc', $encryption_key, 0, $iv);
}else{
$encryption_key = base64_decode($key);
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$encrypted = openssl_encrypt($string, 'aes-256-cbc', $encryption_key, 0, $iv);
return base64_encode($encrypted . '::' . $iv);
}
}
}
if(!function_exists('xml_entity_decode')) {
function xml_entity_decode($s)
{
static $XENTITIES = array('&', '>', '<');
static $XSAFENTITIES = array('#_x_amp#;', '#_x_gt#;', '#_x_lt#;');
$s = str_replace($XENTITIES, $XSAFENTITIES, $s);
$s = html_entity_decode($s, ENT_HTML5 | ENT_NOQUOTES, 'UTF-8');
$s = str_replace($XSAFENTITIES, $XENTITIES, $s);
return $s;
}
}
if(!function_exists('formatBytes')) {
function formatBytes($kbytes, $precision = 2) {
if ($kbytes >= 1048576)
{
$kbytes = number_format($kbytes / 1048576, 1) . ' GB';
}
elseif ($kbytes >= 1024)
{
$kbytes = number_format($kbytes / 1024, 1) . ' MB';
}
elseif ($kbytes >= 1)
{
$kbytes = number_format($kbytes / 1, 1) . ' KB';
}
else
{
$kbytes = '0 bytes';
}
return $kbytes;
}
}
if(!function_exists('searchArray')) {
function searchArray($arr, $keyword)
{
foreach ($arr as $index => $string) {
if (strpos($string, $keyword) !== FALSE)
return $index;
}
}
}
if(!function_exists('get_timezone_offset')) {
function get_timezone_offset($remote_tz, $origin_tz = null)
{
if ($origin_tz === null) {
if (!is_string($origin_tz = date_default_timezone_get())) {
return false;
}
}
$origin_dtz = new DateTimeZone($origin_tz);
$remote_dtz = new DateTimeZone($remote_tz);
$origin_dt = new DateTime("now", $origin_dtz);
$remote_dt = new DateTime("now", $remote_dtz);
$offset = $origin_dtz->getOffset($origin_dt) - $remote_dtz->getOffset($remote_dt);
$offset = $offset / 3600;
return $offset;
}
}
if(!function_exists('FormatdateSchedule')) {
function FormatdateSchedule($formato, $data, $timezone)
{
$formato = (preg_match('/:/i', $data)) ? $formato : str_replace("H:i:s", "", $formato);
$offset = get_timezone_offset('America/Sao_Paulo', $timezone);
$nova_data = strtotime('' . $offset . ' hour', strtotime($data));
$nova_data = date($formato, $nova_data);
return $nova_data;
}
}
if (! function_exists('icecast_auth')) {
/** Credencial de administrador do Icecast, lida uma vez por processo. */
function icecast_auth(): string
{
static $auth = null;
if ($auth === null) {
$set = DB::table('settings')->where('id', '=', 1)->first();
$auth = base64_encode(($set->master_user ?? '') . ':' . ($set->master_pass ?? ''));
}
return $auth;
}
}
if (! function_exists('icecast_admin_many')) {
/**
* Varias chamadas ao admin do Icecast ao mesmo tempo.
*
* O custo aqui e espera de rede, nao processamento: em serie, 200 portas a
* 300 ms viram um minuto so de espera. Com a janela deslizante, o tempo
* total passa a ser o da porta mais lenta, e nao a soma de todas.
*
* $pedidos: chave => ['ip' => ..., 'port' => ..., 'path' => ...]
* Devolve chave => corpo da resposta, ou null quando a porta nao respondeu.
*/
function icecast_admin_many(array $pedidos, int $paralelo = 40): array
{
$saida = [];
if (! $pedidos) {
return $saida;
}
$auth = icecast_auth();
$multi = curl_multi_init();
$abertos = [];
$fila = $pedidos;
$abrir = static function () use (&$fila, &$abertos, $multi, $auth, $paralelo) {
while (count($abertos) < $paralelo && $fila) {
$chave = array_key_first($fila);
$pedido = $fila[$chave];
unset($fila[$chave]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'http://' . $pedido['ip'] . ':' . $pedido['port'] . $pedido['path'],
CURLOPT_HTTPHEADER => ['Authorization: Basic ' . $auth],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 3,
]);
curl_multi_add_handle($multi, $ch);
$abertos[(int) $ch] = [$ch, $chave];
}
};
$abrir();
do {
$andamento = curl_multi_exec($multi, $ativos);
if ($ativos) {
curl_multi_select($multi, 1);
}
while ($pronto = curl_multi_info_read($multi)) {
$ch = $pronto['handle'];
$id = (int) $ch;
$chave = $abertos[$id][1] ?? null;
if ($chave !== null) {
$corpo = curl_multi_getcontent($ch);
$ok = $corpo !== false && (int) curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200;
$saida[$chave] = $ok ? $corpo : null;
}
curl_multi_remove_handle($multi, $ch);
curl_close($ch);
unset($abertos[$id]);
$abrir();
}
} while (($abertos || $fila) && $andamento === CURLM_OK);
curl_multi_close($multi);
return $saida;
}
}
if (! function_exists('icecast_listeners_many')) {
/**
* Ouvintes de varias portas de uma vez.
*
* Duas ondas: primeiro os mounts de todas as portas, depois os ouvintes de
* todos os mounts encontrados. O formato de cada porta e o mesmo que
* icecast_listeners devolve, para o robo nao precisar saber da diferenca.
*
* $alvos: porta => ip do servidor
*/
function icecast_listeners_many(array $alvos, int $paralelo = 40): array
{
$saida = [];
$pedidos = [];
foreach ($alvos as $porta => $ip) {
$saida[$porta] = ['reachable' => false, 'total' => 0, 'mounts' => [], 'listeners' => []];
$pedidos['m|' . $porta] = ['ip' => $ip, 'port' => (int) $porta, 'path' => '/admin/listmounts'];
}
foreach (icecast_admin_many($pedidos, $paralelo) as $chave => $corpo) {
$porta = substr($chave, 2);
$xml = icecast_xml($corpo);
if ($xml === null) {
continue;
}
$saida[$porta]['reachable'] = true;
foreach ($xml->source as $source) {
$nome = (string) $source['mount'];
/* mesmo motivo do icecast_mounts: a contagem daqui nao decide */
if ($nome !== '') {
$saida[$porta]['mounts'][$nome] = (int) ($source->listeners ?? 0);
}
}
}
$pedidos = [];
foreach ($saida as $porta => $dados) {
/* sem listmounts, tenta /live, que e o mount de sempre */
$lista = $dados['mounts'] ? array_keys($dados['mounts']) : ['/live'];
foreach ($lista as $mount) {
$pedidos['l|' . $porta . '|' . $mount] = [
'ip' => $alvos[$porta],
'port' => (int) $porta,
'path' => '/admin/listclients?mount=' . urlencode($mount),
];
}
}
$vistos = [];
foreach (icecast_admin_many($pedidos, $paralelo) as $chave => $corpo) {
[, $porta, $mount] = explode('|', $chave, 3);
$xml = icecast_xml($corpo);
if ($xml === null) {
continue;
}
/* respondeu algo que da para ler: a porta esta de pe */
$saida[$porta]['reachable'] = true;
if (! isset($xml->source->listener)) {
continue;
}
foreach ($xml->source->listener as $listener) {
$id = (string) $listener->ID;
$ip = trim((string) $listener->IP);
/* o id do cliente e unico no servidor, entao isto tambem cobre
o ouvinte pego em dois mounts durante uma troca de fallback */
if ($ip === '' || isset($vistos[$porta][$id])) {
continue;
}
$vistos[$porta][$id] = true;
$saida[$porta]['listeners'][] = [
'id' => $id,
'ip' => $ip,
'mount' => $mount,
'connected' => (int) $listener->Connected,
'useragent' => (string) $listener->UserAgent,
'referer' => isset($listener->Referer) ? (string) $listener->Referer : null,
];
}
}
foreach ($saida as $porta => $dados) {
$saida[$porta]['total'] = count($dados['listeners']);
}
return $saida;
}
}
if (!function_exists('icecast_admin')) {
/**
* Authenticated GET against the Icecast admin API.
* Returns the body, or null when the server did not answer with a 200.
*/
function icecast_admin(string $ip, int $port, string $path): ?string
{
$auth = icecast_auth();
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'http://' . $ip . ':' . $port . $path,
CURLOPT_HTTPHEADER => ['Authorization: Basic ' . $auth],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 3,
]);
$body = curl_exec($ch);
$ok = $body !== false && curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200;
curl_close($ch);
return $ok ? $body : null;
}
}
if (!function_exists('icecast_xml')) {
function icecast_xml(?string $body): ?SimpleXMLElement
{
if ($body === null || $body === '') {
return null;
}
// Icecast copies raw client user agents into the XML, control bytes and
// the NN; escapes it builds for them are both invalid in XML 1.0
$body = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $body);
$body = str_replace('', '', $body);
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string($body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
return $xml === false ? null : $xml;
}
}
if (!function_exists('icecast_mounts')) {
/**
* Mount name => listener count, for mounts that currently hold listeners.
*
* Returns null when the server could not be reached, which callers must
* keep distinct from an empty array: one means "unknown", the other means
* "nobody is listening".
*/
function icecast_mounts(string $ip, int $port): ?array
{
$xml = icecast_xml(icecast_admin($ip, $port, '/admin/listmounts'));
if ($xml === null) {
return null;
}
$mounts = [];
foreach ($xml->source as $source) {
$name = (string) $source['mount'];
/*
* Todo mount entra, inclusive o que se diz vazio: com fallback
* do Liquidsoap esse contador atrasa, e filtrar por ele faz a
* radio inteira sumir da estatistica.
*/
if ($name !== '') {
$mounts[$name] = (int) ($source->listeners ?? 0);
}
}
return $mounts;
}
}
if (!function_exists('robots_statistics')) {
/**
* Kept for callers outside this codebase: same signature and same raw
* listclients XML as before, but the mount is now the one that actually
* holds listeners instead of a fixed guess. Returns 0 on failure, as it
* always did. New code should use icecast_listeners() and read every
* mount, since this can only ever describe one of them.
*/
function robots_statistics($ip, $port)
{
$dados = icecast_listeners((string) $ip, (int) $port);
if (! $dados['reachable']) {
return 0;
}
$blocos = '';
$porMount = [];
foreach ($dados['listeners'] as $ouvinte) {
$porMount[$ouvinte['mount']] = ($porMount[$ouvinte['mount']] ?? 0) + 1;
$blocos .= '' . htmlspecialchars($ouvinte['id'], ENT_XML1)
. '' . htmlspecialchars($ouvinte['ip'], ENT_XML1)
. '' . htmlspecialchars($ouvinte['useragent'], ENT_XML1)
. '' . ($ouvinte['referer'] === null ? '' : '' . htmlspecialchars($ouvinte['referer'], ENT_XML1) . '')
. '' . (int) $ouvinte['connected'] . '';
}
arsort($porMount);
$principal = $porMount ? (string) array_key_first($porMount) : '/live';
/* o vai junto: quem chama usa esse numero como limite do laco */
return '' . count($dados['listeners']) . '' . $blocos . '';
}
}
if (!function_exists('icecast_listeners')) {
/**
* Every listener attached to a streaming port, across all of its mounts.
*
* Which mount holds the audience depends on how the station went live, and
* the name never says it. Going live straight against Icecast source auth
* takes the main mount, and the Liquidsoap autodj keeps its own mount fed
* alongside it. Going live through the Liquidsoap harbor changes nothing on
* the Icecast side at all: same source, same mount, only the audio behind
* it switches. So any fixed choice of mount reports zero in whichever setup
* does not match, and reading the first one is worse still, since Icecast
* keeps mounts in a tree ordered by name and /autodj always wins index 0.
*
* Listener counts already come with listmounts, so every mount holding an
* audience is read and merged. When both mounts are up the listeners on
* each are different people on different URLs, so the sum is the real
* total. Deduping by client id is only a cheap guard.
*
* 'reachable' tells a silent zero apart from an unreachable server.
*/
function icecast_listeners(string $ip, int $port): array
{
$out = ['reachable' => false, 'total' => 0, 'mounts' => [], 'listeners' => []];
$mounts = icecast_mounts($ip, $port);
/*
* Servidor que nao deixa ler o listmounts ainda pode responder o
* listclients: em vez de dar a porta como fora do ar, tenta /live.
*/
$out['reachable'] = $mounts !== null;
$out['mounts'] = $mounts ?: [];
$lista = $mounts === null ? ['/live'] : array_keys($mounts);
$seen = [];
foreach ($lista as $mount) {
$xml = icecast_xml(icecast_admin($ip, $port, '/admin/listclients?mount=' . urlencode($mount)));
if ($xml === null) {
continue;
}
/* respondeu algo que da para ler: a porta esta de pe */
$out['reachable'] = true;
if (!isset($xml->source->listener)) {
continue;
}
foreach ($xml->source->listener as $listener) {
$id = (string) $listener->ID;
$listenerIp = trim((string) $listener->IP);
// Icecast client IDs are unique per server, so this also covers
// a listener caught on two mounts during a fallback switch
if ($listenerIp === '' || isset($seen[$id])) {
continue;
}
$seen[$id] = true;
$out['listeners'][] = [
'id' => $id,
'ip' => $listenerIp,
'mount' => $mount,
'connected' => (int) $listener->Connected,
'useragent' => (string) $listener->UserAgent,
'referer' => isset($listener->Referer) ? (string) $listener->Referer : null,
];
}
}
$out['total'] = count($out['listeners']);
return $out;
}
}
if(!function_exists('listener_label')) {
/* Translate the generic names; brands stay as they are */
function listener_label($name)
{
$keys = [
'Outro' => 'other',
'Site externo' => 'external_site',
'Plataforma BRASCAST' => 'platform',
'Site da rádio' => 'station_site',
'Aplicativo Exclusivo' => 'exclusive_app',
'App Android' => 'android_app',
'App iOS' => 'ios_app',
'Robô' => 'robot',
'Mobile' => 'mobile',
'Servidor' => 'server',
'Samsung Mobile' => 'samsung_mobile',
'LG Mobile' => 'lg_mobile',
'Sony Mobile' => 'sony_mobile',
];
$key = $keys[$name] ?? null;
return $key ? __('radio.source_' . $key) : $name;
}
}
if(!function_exists('source_from_referer')) {
/* Name the source by the page the listener came from */
function source_from_referer($referer, $siteHosts = null)
{
$host = strtolower((string) parse_url((string) $referer, PHP_URL_HOST));
if ($host === '') {
return null;
}
$host = preg_replace('/^www\./', '', $host);
/*
* O proprio painel. Quem escuta pelo player da plataforma aparecia
* como site externo, com o dominio no balaozinho, como se fosse um
* site de terceiro. O endereco do ambiente entra junto para o
* sandbox e o desenvolvimento contarem a mesma coisa.
*/
$plataforma = ['app.brascast.com'];
$proprio = strtolower((string) parse_url((string) config('app.url'), PHP_URL_HOST));
if ($proprio !== '') {
$plataforma[] = preg_replace('/^www\./', '', $proprio);
}
if (in_array($host, $plataforma, true)) {
return 'Plataforma BRASCAST|ico-brascast';
}
$known = [
'radios.com.br' => 'RádiosNet|fa fa-circle-play',
'tunein.com' => 'TuneIn|icon icon-tunein',
'onlineradiobox.com' => 'OnlineRadioBox|fa fa-circle-play',
'mytuner-radio.com' => 'myTuner|fa fa-circle-play',
'streema.com' => 'Streema|fa fa-circle-play',
'zeno.fm' => 'Zeno|fa fa-circle-play',
'radiosnet.com.br' => 'RádiosNet|fa fa-circle-play',
'radio.net' => 'radio.net|fa fa-circle-play',
'radio.de' => 'radio.de|fa fa-circle-play',
'radio.garden' => 'Radio Garden|fa fa-circle-play',
];
foreach ($known as $domain => $name) {
if ($host === $domain || str_ends_with($host, '.' . $domain)) {
return $name;
}
}
/* The station own site deserves its own name */
foreach ((array) $siteHosts as $own) {
$own = strtolower(preg_replace('/^www\./', '', (string) $own));
if ($own !== '' && ($host === $own || str_ends_with($host, '.' . $own))) {
return 'Site da rádio|fa fa-globe-americas|' . mb_substr($host, 0, 60);
}
}
/* Keep the host as detail, shown on hover */
return 'Site externo|fa fa-globe-americas|' . mb_substr($host, 0, 60);
}
}
if(!function_exists('useragent_format')) {
/**
* De quem e o ouvinte, a partir do user agent.
*
* A ordem manda: a primeira regra que casar vence, entao o especifico vem
* antes do generico. O que era uma escada de if agora e tabela, para
* incluir um player novo custar uma linha e nao um ramo no meio do codigo.
*/
function useragent_regras()
{
static $regras = null;
if ($regras !== null) {
return $regras;
}
return $regras = array(
/* aplicativo proprio, antes de qualquer coisa */
array('/BRASCAST_RADIO_UD\s*:\s*iOS/i', 'Aplicativo Exclusivo', 'fab fa-apple'),
array('/BRASCAST_RADIO_UD\s*:\s*Android/i', 'Aplicativo Exclusivo', 'fab fa-android'),
array('/BRASCAST_RADIO_UD/i', 'Aplicativo Exclusivo', 'fab fa-mobile-screen'),
/* players de computador */
array('/VLC|LibVLC/i', 'VLC Player', 'icon icon-vlc'),
array('/NSPlayer|WMFSDK|WMPlayer/i', 'Win Media Player', 'icon icon-winmp'),
array('/RMA|RealMedia/i', 'Real Player', 'icon icon-realplayer'),
array('/(?=.*WinampMPEG)(?=.*Ultravox)/is', 'Winamp', 'icon icon-winamp'),
array('/QuickTime/i', 'QuickTime', 'icon icon-quicktime'),
/* android sem navegador por tras */
array('/^(?!.*Mozilla)(?=.*(?:EMPTY|Dalvik))/is', 'App Android', 'fab fa-android'),
array('/iTunes/i', 'iTunes', 'fab fa-itunes'),
/* agregadores */
array('/RadiosNet/i', 'RádiosNet', 'fa fa-circle-play'),
array('/RadioGarden/i', 'Radio Garden', 'fa fa-circle-play'),
array('/SimpleRadio|streema/i', 'Simple Radio', 'fa fa-circle-play'),
array('/RadioBOSS/i', 'RadioBOSS', 'fa fa-circle-play'),
array('/bot\b|crawler|spider/i', 'Robô', 'fa fa-robot'),
array('/SmartTV|Smart-TV|NetCast|Tizen|Web0S|HbbTV/i', 'Smart TV', 'fa fa-tv'),
array('/TuneIn/i', 'TuneIn', 'icon icon-tunein'),
array('/AIMP/i', 'AIMP', 'fa fa-circle-play'),
array('/foobar2000/i', 'foobar2000', 'fa fa-circle-play'),
array('/Kodi|XBMC/i', 'Kodi', 'fa fa-circle-play'),
array('/mpg123/i', 'mpg123', 'fa fa-circle-play'),
array('/MPlayer/i', 'MPlayer', 'fa fa-circle-play'),
array('/mpv/i', 'mpv', 'fa fa-circle-play'),
array('/myTuner/i', 'myTuner', 'fa fa-circle-play'),
array('/Transistor/i', 'Transistor', 'fa fa-circle-play'),
/* caixas de som e televisao */
array('/Cast Lite/i', 'Cast Lite', 'fa fa-tv'),
array('/Sonos/i', 'Sonos', 'fa fa-volume-high'),
array('/Roku/i', 'Roku', 'fa fa-tv'),
array('/Alexa|Echo/i', 'Alexa', 'fa fa-microphone'),
array('/CastDevice|Chromecast|Google-Speech|GoogleHome/i', 'Google Cast', 'fa fa-tv'),
/* bibliotecas de aplicativo */
array('/ExoPlayer|okhttp|Dalvik/i', 'App Android', 'fab fa-android'),
array('/AppleCoreMedia|CFNetwork/i', 'App iOS', 'fab fa-apple'),
array('/Winamp/i', 'Winamp', 'icon icon-winamp'),
/*
* Lavf e FFmpeg contavam como TuneIn. O TuneIn se identifica pelo
* nome na regra acima; isto aqui e qualquer player feito em cima do
* ffmpeg, entao ganhou rotulo proprio.
*/
array('/Lavf|FFmpeg/i', 'FFmpeg', 'fa fa-circle-play'),
/* aparelhos. SM- e LG so contam como modelo, nao no meio de palavra */
array('/Samsung|\bSM-[A-Z0-9]/i', 'Samsung Mobile', 'icon icon-samsung-l'),
array('/Android/i', 'Android', 'fab fa-android'),
array('/RTMP/i', 'RTMP', 'fa fa-circle-play'),
/* navegadores, por ultimo: quase todo mundo se diz Mozilla */
array('/OPR/i', 'Opera', 'fab fa-opera'),
array('/Edg/i', 'Edge', 'fab fa-edge'),
array('/Chrome/i', 'Chrome', 'fab fa-chrome'),
array('/Safari/i', 'Safari', 'fab fa-safari'),
array('/Firefox/i', 'Firefox', 'fab fa-firefox'),
array('/MSIE|Trident/i', 'Explorer', 'fab fa-internet-explorer'),
array('/Sony/i', 'Sony Mobile', 'fab fa-android'),
array('/\bLG\b|LG-[A-Z0-9]/i', 'LG Mobile', 'fa fa-circle-play'),
);
}
function useragent_format($useragent, $referer = null, $siteHosts = null)
{
/*
* O texto vem do ouvinte, entao pode vir com qualquer tamanho. As
* regras usam lookahead, e num texto de 100 mil caracteres cada
* classificacao levava 2,6s; com o corte, fica em milesimos. Nenhum
* player real se identifica em mais de 512 caracteres.
*/
$useragent = mb_substr((string) $useragent, 0, 512);
$fromSite = source_from_referer($referer, $siteHosts);
$browsers = '/OPR|Edg|Chrome|Safari|Firefox|MSIE|Trident/i';
/* Referrer beats a plain browser user agent */
if ($fromSite !== null && (trim($useragent) === '' || preg_match($browsers, $useragent))) {
return $fromSite;
}
foreach (useragent_regras() as $regra) {
if (preg_match($regra[0], $useragent)) {
return $regra[1] . '|' . $regra[2];
}
}
/* o que ninguem reconheceu fica registrado, para virar regra depois */
useragent_unknown($useragent);
return 'Outro|fa fa-circle-question';
}
}
if (!function_exists('useragent_unknown')) {
/**
* Guarda o user agent que nenhuma regra reconheceu.
*
* E o unico jeito de saber o que falta cobrir sem adivinhar. Grava uma vez
* por texto e por processo, entao o robo de estatisticas nao paga uma
* escrita por ouvinte, e some sozinho se a tabela ainda nao existir.
*/
function useragent_unknown($useragent)
{
static $existe = null;
static $vistos = array();
$useragent = mb_substr(trim((string) $useragent), 0, 500);
if ($useragent === '' || isset($vistos[$useragent])) {
return;
}
$vistos[$useragent] = true;
if ($existe === null) {
$existe = \Illuminate\Support\Facades\Schema::hasTable('statistics_useragents');
}
if (!$existe) {
return;
}
try {
\Illuminate\Support\Facades\DB::statement(
'INSERT INTO statistics_useragents (hash, useragent, hits, first_seen, last_seen)'
. ' VALUES (?, ?, 1, NOW(), NOW())'
. ' ON DUPLICATE KEY UPDATE hits = hits + 1, last_seen = NOW()',
array(md5($useragent), $useragent)
);
} catch (\Throwable $erro) {
/* registrar o desconhecido nunca pode derrubar a coleta */
}
}
}
if(!function_exists('getOS')) {
if (!function_exists('browser_name')) {
/* Browser shown next to the system on the devices list */
function browser_name($useragent)
{
$useragent = (string) $useragent;
if (preg_match('/OPR|Opera/i', $useragent)) { return 'Opera'; }
if (preg_match('/Edg/i', $useragent)) { return 'Edge'; }
if (preg_match('/Chrome|CriOS/i', $useragent)) { return 'Chrome'; }
if (preg_match('/Firefox|FxiOS/i', $useragent)) { return 'Firefox'; }
if (preg_match('/Safari/i', $useragent)) { return 'Safari'; }
if (preg_match('/MSIE|Trident/i', $useragent)) { return 'Internet Explorer'; }
return __('radio.source_other');
}
}
function getOS($user_agent) {
$os_platform = "Outro|fa fa-circle-question";
$os_array = array(
'/Windows NT 10.0/i' => 'Windows 10/11|fab fa-windows',
'/windows nt 6.2/i' => 'Windows 8|fab fa-windows',
'/windows nt 6.1/i' => 'Windows 7|fab fa-windows',
'/windows nt 6.0/i' => 'Windows Vista|icon icon-win-old',
'/windows nt 5.2/i' => 'Windows 2003/XP x64|icon icon-win-old',
'/windows nt 5.1/i' => 'Windows XP|icon icon-win-old',
'/windows xp/i' => 'Windows XP|icon icon-win-old',
'/windows nt 5.0/i' => 'Windows 2000|icon icon-win-old',
'/windows me/i' => 'Windows ME|fab fa-windows',
'/win98/i' => 'Windows 98|icon icon-win-old',
'/win95/i' => 'Windows 95|icon icon-win-old',
'/win16/i' => 'Windows 3.11|icon icon-win-old',
'/macintosh|mac os x/i' => 'Mac OS X|icon icon-macos',
'/mac_powerpc/i' => 'Mac OS 9|icon icon-macos',
'/linux/i' => 'Linux|fab fa-linux',
'/ubuntu/i' => 'Ubuntu|fab fa-ubuntu',
'/iphone/i' => 'iPhone|fab fa-apple',
'/ipod/i' => 'iPod|icon icon-ipod',
'/ipad/i' => 'iPad|fab fa-tablet-button',
'/android/i' => 'Android|fab fa-android',
'/blackberry/i' => 'BlackBerry|fab fa-blackberry',
'/webos/i' => 'Mobile|fab fa-mobile-screen',
'/cros/i' => 'ChromeOS|fab fa-chrome',
'/roku/i' => 'Roku|fa fa-tv',
'/smarttv|smart-tv|netcast|tizen|web0s|hbbtv/i' => 'Smart TV|fa fa-tv',
'/BRASCAST_RADIO_UD\s*:\s*iOS/i' => 'iOS|fab fa-apple',
'/BRASCAST_RADIO_UD\s*:\s*Android/i' => 'Android|fab fa-android',
'/okhttp|Transistor|com\.streema\.simpleradio/i' => 'Android|fab fa-android',
'/com\.simpleradio\./i' => 'iOS|fab fa-apple',
'/RadioBOSS/i' => 'Windows|fab fa-windows',
'/bot\b|crawler|spider/i' => 'Servidor|fa fa-server',
'/Echo\/[0-9]/i' => 'Amazon Echo|fab fa-amazon'
);
foreach ($os_array as $regex => $value) {
if (preg_match($regex, $user_agent)) {
$os_platform = $value;
}
}
return $os_platform;
}
}
if (!function_exists('geoip_desconhecido')) {
function geoip_desconhecido(): array
{
return [
'country_code' => 'XX',
'country_name' => 'Desconhecido',
'state' => 'XX',
'state_code' => 'XX',
'city' => 'Desconhecida',
'latitude' => 0,
'longitude' => 0,
];
}
}
if (!function_exists('geoip_pais')) {
/**
* Country name in the language the panel is being viewed in.
*
* The stored name is whatever locale resolved the IP at the time, so it is
* only a fallback. The ISO code is the stable part, and intl turns it into
* a localized name at no cost, which is why country is the one geo field
* that can be multilingual without touching the schema.
*
* City and state stay as stored: they are proper nouns, and a per language
* column would mean altering tables that already carry years of rows.
*/
function geoip_pais(?string $codigo, ?string $fallback = null): string
{
$codigo = strtoupper(trim((string) $codigo));
if ($codigo === '' || $codigo === 'XX') {
return $fallback ?: __('general.geoip_country_unknown');
}
if (! class_exists('Locale')) {
return $fallback ?: $codigo;
}
$locale = app()->getLocale();
$nome = Locale::getDisplayRegion('-' . $codigo, $locale);
/**
* Two ways intl can miss: it echoes the code back, or it resolves to
* the CLDR "unknown region" label, which is a translated string and so
* cannot be matched literally. Comparing against ZZ catches the second.
*/
$desconhecido = Locale::getDisplayRegion('-ZZ', $locale);
if (! $nome || $nome === $codigo || $nome === $desconhecido) {
return $fallback ?: $codigo;
}
return $nome;
}
}
if (!function_exists('geoip_maxmind')) {
/**
* Local GeoLite2 lookup. No network, so the statistics robot can resolve a
* whole listener list without leaving the machine.
*
* The Reader is held open for the request: it memory maps the database, and
* reopening it per IP would throw that away on every listener.
*/
function geoip_maxmind(string $ip): ?array
{
static $reader = false;
if (! filter_var($ip, FILTER_VALIDATE_IP)) {
return null;
}
if ($reader === false) {
$arquivo = config('geoip.database');
$reader = null;
if (is_file($arquivo) && class_exists(GeoIp2\Database\Reader::class)) {
try {
$reader = new GeoIp2\Database\Reader($arquivo, [config('geoip.locale'), 'en']);
} catch (Throwable $e) {
Log::warning('geoip: nao consegui abrir a base MaxMind', ['erro' => $e->getMessage()]);
}
}
}
if (! $reader) {
return null;
}
try {
$registro = $reader->city($ip);
} catch (Throwable $e) {
// AddressNotFoundException is the normal miss, anything else is too
return null;
}
// mostSpecificSubdivision, nao subdivisions[0]: paises com subdivisao
// aninhada trazem a regiao ampla primeiro e o estado util por ultimo
$estado = $registro->mostSpecificSubdivision;
if (! $registro->city->name && ! $registro->country->isoCode) {
return null;
}
return [
'country_code' => $registro->country->isoCode ?: 'XX',
'country_name' => $registro->country->name ?: 'Desconhecido',
'state' => $estado->name ?? 'XX',
'state_code' => $estado->isoCode ?? 'XX',
'city' => $registro->city->name ?: 'Desconhecida',
'latitude' => $registro->location->latitude ?? 0,
'longitude' => $registro->location->longitude ?? 0,
];
}
}
if (!function_exists('geoip_invertexto')) {
function geoip_invertexto(string $ip): ?array
{
if (! config('geoip.fallback.enabled')) {
return null;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.invertexto.com/v1/geoip/' . urlencode($ip) . '?token=' . urlencode(config('geoip.fallback.token')),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_USERAGENT => 'Mozilla/5.0',
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200 || ! $resposta) {
return null;
}
$dados = json_decode($resposta, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($dados)) {
return null;
}
return [
'country_code' => $dados['country_code'] ?? 'XX',
'country_name' => $dados['country'] ?? 'Desconhecido',
'state' => $dados['state'] ?? 'XX',
'state_code' => $dados['state_code'] ?? 'XX',
'city' => $dados['city'] ?? 'Desconhecida',
'latitude' => $dados['latitude'] ?? 0,
'longitude' => $dados['longitude'] ?? 0,
];
}
}
if (!function_exists('api_geoip')) {
/**
* Resolves an IP through three layers: the geoip table, the local MaxMind
* database, then the invertexto API. Only the last one costs a request, and
* every answer is cached in the table, so an IP is paid for once ever.
*
* MaxMind only short circuits on a city level hit. Backbone and datacenter
* ranges come back country only, and the API resolves some of those, so
* they are worth the request. If the API is down the partial MaxMind record
* is still better than nothing.
*
* Values are plain UTF-8. They used to be run through utf8_decode() and
* addslashes(): the first is deprecated since 8.2 and corrupted accented
* city names, the second escaped for a raw query no caller writes, so the
* backslashes were being stored literally.
*/
function api_geoip($ip)
{
$ip = (string) $ip;
$record = DB::table('geoip')->where('ip', $ip)->first();
if ($record) {
return [
'country_code' => $record->country_code,
'country_name' => $record->country_name,
'state' => $record->state,
'state_code' => $record->state_code,
'city' => $record->city,
'latitude' => $record->lat,
'longitude' => $record->lon,
];
}
$maxmind = geoip_maxmind($ip);
if ($maxmind && $maxmind['city'] !== 'Desconhecida') {
$dados = $maxmind;
} else {
$dados = geoip_invertexto($ip) ?? $maxmind ?? geoip_desconhecido();
}
DB::table('geoip')->insert([
'ip' => $ip,
'country_code' => $dados['country_code'],
'country_name' => $dados['country_name'],
'state' => $dados['state'],
'state_code' => $dados['state_code'],
'city' => $dados['city'],
'lat' => $dados['latitude'],
'lon' => $dados['longitude'],
]);
return $dados;
}
}
if (!function_exists('yt_conv')) {
function yt_conv($url)
{
$parts = parse_url(trim($url));
if (!isset($parts['host'])) {
return null;
}
if (!empty($parts['query'])) {
parse_str($parts['query'], $query);
if (!empty($query['v'])) {
return $query['v'];
}
}
if (strpos($parts['host'], 'youtu.be') !== false) {
return trim($parts['path'], '/');
}
$path = trim($parts['path'] ?? '', '/');
$segments = explode('/', $path);
if (count($segments) >= 2 && in_array($segments[0], ['shorts', 'live'])) {
return $segments[1];
}
return null;
}
}
if(!function_exists('time_ago')) {
function time_ago($datetime, $full = false)
{
$now = new DateTime;
$ago = new DateTime($datetime);
$diff = $now->diff($ago);
$diff->w = floor($diff->d / 7);
$diff->d -= $diff->w * 7;
$string = array(
'y' => 'general.time_unit_year',
'm' => 'general.time_unit_month',
'w' => 'general.time_unit_week',
'd' => 'general.time_unit_day',
'h' => 'general.time_unit_hour',
'i' => 'general.time_unit_minute',
's' => 'general.time_unit_second',
);
foreach ($string as $k => &$v) {
if ($diff->$k) {
$v = trans_choice($v, $diff->$k, ['count' => $diff->$k]);
} else {
unset($string[$k]);
}
}
if (!$full) $string = array_slice($string, 0, 1);
return $string ? __('general.time_ago', ['time' => implode(', ', $string)]) : __('general.time_now');
}
}
if(!function_exists('SendMail')) {
function SendMail($template,$user_id,$additionals = null){
$arrTitle = array(
'activate' => 'Convite para participar da rádio:', 'aceptedAdmin' => 'Rádio adicionada:', 'removedAdmin' => 'Convite removido da rádio:',
'appRejected' => 'Aplicativo rejeitado pelo Google Play', 'appPublished' => 'Aplicativo Disponível');
$object = new stdClass();
$object->add = (isset($additionals) ? $additionals : null);
$Mailuser = DB::table('users')->where('id','=',$user_id)->get()->first();
$idioma = in_array($Mailuser->lang ?? '', config('app.available_locales', []), true)
? $Mailuser->lang
: config('app.locale');
$arrTitle['recoverPassword'] = __('general.mail_recover_subject', [], $idioma);
$temp = new stdClass();
$temp->name = $template;
\Mail::send(new \App\Mail\SysMail($Mailuser,$temp,$arrTitle[$template],$object));
}
}
if(!function_exists('setLog')) {
function setLog($port, $action, $ip)
{
DB::table('logs')->insert([
'port' => $port,
'action' => $action,
'user' => Auth::id() ?? null,
'adm' => session('admID') ?? null,
'ip' => $ip,
'created_at' => NOW()
]);
}
}
if (! function_exists('site_row')) {
/**
* Row of the session site, cached per request.
*
* The menu needs a couple of columns to decide which entries make sense
* for this site, and it renders on every page: reading it once here keeps
* that from becoming one query per menu build.
*/
function site_row()
{
/* o cache proprio saiu: quem guarda a linha agora e o SiteContext */
return \App\Services\SiteContext::current();
}
}
if (! function_exists('assetv')) {
/**
* Asset URL stamped with the file mtime.
*
* There is no build step here, so a changed css or js would stay in the
* browser cache until a hard reload. The stamp only moves when the file
* moves.
*/
function assetv($path)
{
$file = public_path(ltrim($path, '/'));
return asset($path) . (is_file($file) ? '?v=' . filemtime($file) : '');
}
}
if (! function_exists('user_site_links')) {
/**
* Sites a user may open, with the role each link grants.
*
* Two things make a link real: owning the site, or an invite that was
* confirmed. A row in user_sites alone is not enough, an invite that was
* never confirmed stays there and must not open anything.
*/
function user_site_links($userId)
{
$links = DB::table('user_sites')->where('user_id', '=', $userId)->get(['site_id', 'server_id']);
$out = [];
foreach ($links->groupBy('server_id') as $serverId => $group) {
$ids = collect($group)->pluck('site_id')->unique()->values()->all();
$owners = DB::connection('mysql_site_' . $serverId)->table('sites')
->whereIn('id', $ids)->pluck('user_id', 'id');
$invites = DB::connection('mysql_site_' . $serverId)->table('administrators')
->where('user_id', '=', $userId)->whereIn('site_id', $ids)
->pluck('active', 'site_id');
foreach ($ids as $siteId) {
if (! isset($owners[$siteId])) {
continue;
}
if ((int) $owners[$siteId] === (int) $userId) {
$role = 'owner';
} elseif (! empty($invites[$siteId])) {
$role = 'admin';
} else {
continue;
}
$out[] = ['site_id' => (int) $siteId, 'server_id' => (int) $serverId, 'role' => $role];
}
}
return $out;
}
}
if (! function_exists('user_owns_any_site')) {
/** Whether the user owns a site, which is what opens the owner screens. */
function user_owns_any_site($userId)
{
foreach (user_site_links($userId) as $link) {
if ($link['role'] === 'owner') {
return true;
}
}
return false;
}
}
if (! function_exists('site_role')) {
/**
* Role the logged user has on the site of the session.
*
* Reads the relationship, never users.type: that column is stamped once at
* sign up, so an invited admin who later bought a radio kept being treated
* as an admin on a site that is theirs.
*/
function site_role()
{
$site = site_row();
if (! $site) {
return null;
}
if ((int) $site->user_id === (int) Auth::id()) {
return 'owner';
}
$invite = DB::connection('mysql_site_' . session('serverID'))->table('administrators')
->where('site_id', '=', session('siteID'))
->where('user_id', '=', Auth::id())->first();
return $invite && $invite->active ? 'admin' : null;
}
}
if (! function_exists('menu_ativo')) {
/**
* A pagina em que estou, para o menu acender o item certo.
*
* Compara pelo nome da rota, e nao pela URL, porque a mesma tela aparece com
* parametros diferentes. O ponto no fim evita que site.posts marque
* site.postsAlgo, e o sufixo .r ou .u sai para que as rotas irmas da tela
* (o do, o delete) tambem contem como a mesma pagina.
*
* @param string[]|null $match nomes-base proprios, quando a tela responde por mais de um
*/
function menu_ativo(string $rota, ?array $match = null): bool
{
$atual = (string) Route::currentRouteName();
if ($atual === '' || $rota === '') {
return false;
}
foreach ($match ?: [preg_replace('/\.(r|u)$/', '', $rota)] as $base) {
if ($atual === $base || str_starts_with($atual, $base . '.')) {
return true;
}
}
return false;
}
}
if (! function_exists('site_subnav_groups')) {
/**
* Groups of the site sub-menu, with the current page already marked.
*
* The header and the mobile drawer render this same list. It lives here
* because when each one decided on its own what to show, the two menus
* drifted apart.
*/
function site_subnav_groups()
{
$site = site_row();
if (! $site) {
return [];
}
/* a TV nao tem pedido de musica, top musicas nem player proprio */
$isRadio = ! isset($site->stream_tv_name);
$hasStream = $isRadio || ! empty($site->stream_port);
$current = (string) Route::currentRouteName();
$groups = [
[
'label' => __('site.menu_item_2'),
'icon' => 'fa-gear',
'items' => [
['label' => __('site.menu_item_2_sub_1'), 'icon' => 'fa-circle-info', 'route' => 'site.config.r'],
['label' => __('site.menu_item_2_sub_2'), 'icon' => 'fa-tags', 'route' => 'site.meta-tags.r'],
['label' => __('site.menu_item_2_sub_5'), 'icon' => 'fa-code', 'route' => 'site.head-tags.r'],
['label' => __('site.menu_item_2_sub_4'), 'icon' => 'fa-sitemap', 'route' => 'site.sitemap.r'],
],
],
[
'label' => __('site.menu_item_3'),
'icon' => 'fa-newspaper',
'items' => [
['label' => __('site.menu_item_3_sub_1'), 'icon' => 'fa-newspaper', 'route' => 'site.posts.r'],
['label' => __('site.menu_item_3_sub_2'), 'icon' => 'fa-film', 'route' => 'site.videos.r'],
['label' => __('site.menu_item_3_sub_3'), 'icon' => 'fa-images', 'route' => 'site.album.r'],
['label' => __('site.menu_item_3_sub_4'), 'icon' => 'fa-star', 'route' => 'site.featured.r'],
['label' => __('site.menu_item_3_sub_5'), 'icon' => 'fa-sliders', 'route' => 'site.slider.r'],
['label' => __('site.menu_item_3_sub_6'), 'icon' => 'fa-calendar-days', 'route' => 'site.events.r'],
['label' => __('site.menu_item_3_sub_7'), 'icon' => 'fa-bullhorn', 'route' => 'site.ads.r'],
['label' => __('site.menu_item_3_sub_8'), 'icon' => 'fa-puzzle-piece', 'route' => 'site.free-modules.r'],
['label' => __('site.menu_item_3_sub_9'), 'icon' => 'fa-file-lines', 'route' => 'site.pages.r'],
['label' => __('site.menu_item_3_sub_10'), 'icon' => 'fa-link', 'route' => 'site.links.r'],
['label' => __('site.menu_item_3_sub_11'), 'icon' => 'fa-music', 'route' => 'site.top-music.r', 'when' => $hasStream],
['label' => __('site.menu_item_7'), 'icon' => 'fa-microphone', 'route' => 'site.podcasts.r',
'when' => $hasStream, 'match' => ['site.podcasts', 'site.podcastsEps']],
],
],
[
'label' => __('site.menu_item_4'),
'icon' => 'fa-calendar-week',
'items' => [
['label' => __('site.menu_item_4_sub_1'), 'icon' => 'fa-calendar-week', 'route' => 'site.scheduling.r',
'match' => ['site.scheduling', 'site.schedules']],
['label' => __('site.menu_item_4_sub_2'), 'icon' => 'fa-user', 'route' => 'site.broadcasters.r'],
['label' => __('site.menu_item_4_sub_3'), 'icon' => 'fa-list', 'route' => 'site.programs.r'],
],
],
[
'label' => __('site.menu_item_5'),
'icon' => 'fa-comments',
'items' => [
['label' => __('site.menu_item_5_sub_1'), 'icon' => 'fa-video', 'route' => 'site.camera.r',
'when' => ! Config::get('constants.IS_APP') && ! empty($site->stream_server) && ! empty($site->stream_port)],
['label' => __('site.menu_item_5_sub_2'), 'icon' => 'fa-comments', 'route' => 'site.chat.r'],
['label' => __('site.menu_item_5_sub_3'), 'icon' => 'fa-file-signature', 'route' => 'site.music-requests.r', 'when' => $isRadio],
['label' => __('site.menu_item_5_sub_4'), 'icon' => 'fa-envelope-open-text', 'route' => 'site.messages.r'],
['label' => __('site.menu_item_5_sub_5'), 'icon' => 'fa-comment-dots', 'route' => 'site.comments.r'],
['label' => __('site.menu_item_5_sub_6'), 'icon' => 'fa-list-check', 'route' => 'site.polls.r'],
['label' => __('site.menu_item_5_sub_7'), 'icon' => 'fa-medal', 'route' => 'site.promotions.r'],
['label' => __('site.menu_item_5_sub_8'), 'icon' => 'fa-text-width', 'route' => 'site.forms.r'],
],
],
[
'label' => __('site.menu_item_6'),
'icon' => 'fa-paintbrush',
'items' => [
['label' => __('site.menu_item_6_sub_1'), 'icon' => 'fa-table', 'route' => 'site.theme.themes.r'],
['label' => __('site.menu_item_6_sub_2'), 'icon' => 'fa-paintbrush', 'route' => 'site.theme.customize.u'],
['label' => __('site.menu_item_6_sub_3'), 'icon' => 'fa-share-from-square', 'route' => 'site.theme.share.u'],
['label' => __('site.menu_item_6_sub_4'), 'icon' => 'fa-image', 'route' => 'site.theme.bg.u'],
['label' => __('site.menu_item_6_sub_5'), 'icon' => 'fa-earth-americas', 'route' => 'site.theme.icon.u'],
/* a TV com radio junto continua tendo player proprio */
['label' => __('site.menu_item_6_sub_6'), 'icon' => 'fa-circle-play', 'route' => 'site.theme.players.r', 'when' => $hasStream],
],
],
];
$out = [];
foreach ($groups as $group) {
$items = [];
$active = false;
foreach ($group['items'] as $item) {
if (array_key_exists('when', $item) && ! $item['when']) {
continue;
}
if (! Route::has($item['route'])) {
continue;
}
$on = menu_ativo($item['route'], $item['match'] ?? null);
$active = $active || $on;
$items[] = [
'label' => $item['label'],
'icon' => $item['icon'],
'url' => route($item['route']),
'active' => $on,
];
}
if (! $items) {
continue;
}
$out[] = [
'label' => $group['label'],
'icon' => $group['icon'],
'active' => $active,
'items' => $items,
];
}
return $out;
}
}
if (! function_exists('site_server')) {
/**
* Server row of the session site.
*
* The header needs the server name to mount the subdomain, and screens
* that already carry $serverData pass it in instead of asking again.
*/
function site_server()
{
static $cache = [];
$id = (int) session('serverID');
if (! $id) {
return null;
}
if (! array_key_exists($id, $cache)) {
$cache[$id] = DB::table('sites_servers')->where('id', '=', $id)->first();
}
return $cache[$id];
}
}
if (! function_exists('user_themes')) {
/** The themes the panel ships, and the only values accepted on save. */
function user_themes()
{
return ['light', 'dark'];
}
}
if (! function_exists('user_theme')) {
/**
* Theme chosen by the logged user.
*
* Falls back to the light one while users.theme does not exist yet: the
* column arrives by ALTER, and the panel has to stand up before it.
*/
function user_theme()
{
$theme = Auth::user()->theme ?? null;
return in_array($theme, user_themes(), true) ? $theme : 'light';
}
}
if (! function_exists('social_links')) {
/**
* Social networks the site filled in, ready to render.
*
* Only what has an address comes back, so the caller never decides what to
* hide.
*/
function social_links($social)
{
$networks = [
'facebook' => ['label' => 'Facebook', 'icon' => 'fa-facebook-f'],
'instagram' => ['label' => 'Instagram', 'icon' => 'fa-instagram'],
'twitter' => ['label' => 'X', 'icon' => 'fa-x-twitter'],
'youtube' => ['label' => 'YouTube', 'icon' => 'fa-youtube'],
'tiktok' => ['label' => 'TikTok', 'icon' => 'fa-tiktok'],
'whatsapp' => ['label' => 'WhatsApp', 'icon' => 'fa-whatsapp'],
'telegram' => ['label' => 'Telegram', 'icon' => 'fa-telegram'],
'twitch' => ['label' => 'Twitch', 'icon' => 'fa-twitch'],
'linkedin' => ['label' => 'LinkedIn', 'icon' => 'fa-linkedin-in'],
'pinterest' => ['label' => 'Pinterest', 'icon' => 'fa-pinterest-p'],
'snapchat' => ['label' => 'Snapchat', 'icon' => 'fa-snapchat'],
'soundcloud' => ['label' => 'SoundCloud', 'icon' => 'fa-soundcloud'],
'mixcloud' => ['label' => 'Mixcloud', 'icon' => 'fa-mixcloud'],
'lastfm' => ['label' => 'Last.fm', 'icon' => 'fa-lastfm'],
];
$out = [];
foreach ($networks as $key => $network) {
$url = $social->{'social_' . $key} ?? null;
if (empty($url)) {
continue;
}
$out[] = ['url' => $url, 'label' => $network['label'], 'icon' => $network['icon']];
}
return $out;
}
}
if (! function_exists('general_subnav_items')) {
/**
* Tabs of the general area, with the current page already marked.
*/
function general_subnav_items()
{
$current = (string) Route::currentRouteName();
$tabs = [
['label' => __('general.breadcrumb_home'), 'icon' => 'fa-house', 'route' => 'dashboard.r'],
['label' => __('general.page_users_h1'), 'icon' => 'fa-users', 'route' => 'users.r'],
['label' => __('general.page_emails_h1'), 'icon' => 'fa-envelope', 'route' => 'emails.r'],
/* o convidado nao tem lista de radios propria: ele escolhe entre as que administra */
['label' => __('general.page_stations_h1'), 'icon' => 'fa-border-all', 'route' => (string) (Auth::user()->type ?? '') === '2' ? 'selectSite' : 'stations.r'],
['label' => __('general.page_settings_h1'), 'icon' => 'fa-sliders', 'route' => 'settings.r'],
];
/* the trail is the owner's reading of the station, so only they see the tab */
if (site_role() === 'owner') {
$tabs[] = ['label' => __('general.activities_nav'), 'icon' => 'fa-clock-rotate-left', 'route' => 'activities.r'];
}
$out = [];
foreach ($tabs as $tab) {
if (! Route::has($tab['route'])) {
continue;
}
$base = preg_replace('/\.r$/', '', $tab['route']);
$out[] = [
'label' => $tab['label'],
'icon' => $tab['icon'],
'url' => route($tab['route']),
'active' => $current === $tab['route'] || str_starts_with($current, $base . '.'),
];
}
return $out;
}
}
if (! function_exists('permission_groups')) {
/**
* The permission tree the invite screen draws.
*
* The item key is the second piece of the route name on purpose: the
* middleware builds what it looks for out of the route it is guarding, so
* naming them apart here is what makes a granted permission stop working.
*/
function permission_groups($hasTv = false, $hasStream = true)
{
$groups = [
'general' => [
'label' => __('general.top_menu_item_general'),
'icon' => 'fa-gauge-high',
/* sem prefixo: a rota destas telas tem uma parte so */
'prefix' => '',
'sections' => [
'main' => [
'label' => __('general.top_menu_item_general'),
'items' => [
'dashboard' => ['label' => __('general.perm_dashboard'), 'icon' => 'fa-gauge-high'],
'emails' => ['label' => __('general.perm_emails'), 'icon' => 'fa-envelope'],
'settings' => ['label' => __('general.perm_settings'), 'icon' => 'fa-sliders'],
],
],
],
],
'site' => [
'label' => __('general.header_nav_web'),
'icon' => 'fa-globe',
'prefix' => 'site',
'sections' => [
'general' => [
'label' => __('site.menu_item_2'),
'items' => [
'home' => ['label' => __('general.perm_home'), 'icon' => 'fa-house'],
'config' => ['label' => __('general.perm_site_details'), 'icon' => 'fa-circle-info'],
'meta-tags' => ['label' => __('general.perm_metatags'), 'icon' => 'fa-tags'],
'hotsite' => ['label' => __('general.perm_hotsite'), 'icon' => 'fa-star'],
'sitemap' => ['label' => __('general.perm_sitemap'), 'icon' => 'fa-sitemap'],
'head-tags' => ['label' => __('site.menu_item_2_sub_5'), 'icon' => 'fa-code'],
'pagelink' => ['label' => __('general.header_web_startpage_title'), 'icon' => 'fa-link'],
],
],
'content' => [
'label' => __('site.menu_item_3'),
'items' => [
'posts' => ['label' => __('general.perm_posts'), 'icon' => 'fa-newspaper'],
'videos' => ['label' => __('general.perm_videos'), 'icon' => 'fa-film'],
'album' => ['label' => __('general.perm_gallery'), 'icon' => 'fa-images'],
'featured' => ['label' => __('general.perm_featured'), 'icon' => 'fa-star'],
'slider' => ['label' => __('general.perm_slider'), 'icon' => 'fa-sliders'],
'events' => ['label' => __('general.perm_events'), 'icon' => 'fa-calendar-days'],
'ads' => ['label' => __('general.perm_ads'), 'icon' => 'fa-bullhorn'],
'free-modules' => ['label' => __('general.perm_freemodule'), 'icon' => 'fa-puzzle-piece'],
'pages' => ['label' => __('general.perm_pages'), 'icon' => 'fa-file-lines'],
'links' => ['label' => __('general.perm_links'), 'icon' => 'fa-link'],
'top-music' => ['label' => __('general.perm_topmusic'), 'icon' => 'fa-music'],
'podcasts' => ['label' => __('general.perm_podcasts'), 'icon' => 'fa-microphone'],
],
],
'schedule' => [
'label' => __('site.menu_item_4'),
'items' => [
'scheduling' => ['label' => __('general.perm_schedule'), 'icon' => 'fa-calendar-week'],
'broadcasters' => ['label' => __('general.perm_broadcasters'), 'icon' => 'fa-user'],
'programs' => ['label' => __('general.perm_programs'), 'icon' => 'fa-list'],
],
],
'interactivity' => [
'label' => __('site.menu_item_5'),
'items' => [
'camera' => ['label' => __('general.perm_camera'), 'icon' => 'fa-video'],
'chat' => ['label' => __('general.perm_chat'), 'icon' => 'fa-comments'],
'music-requests' => ['label' => __('general.perm_music_requests'), 'icon' => 'fa-file-signature'],
'messages' => ['label' => __('general.perm_messages'), 'icon' => 'fa-envelope-open-text'],
'comments' => ['label' => __('general.perm_comments'), 'icon' => 'fa-comment-dots'],
'polls' => ['label' => __('general.perm_polls'), 'icon' => 'fa-list-check'],
'promotions' => ['label' => __('general.perm_promotions'), 'icon' => 'fa-medal'],
'forms' => ['label' => __('general.perm_forms'), 'icon' => 'fa-text-width'],
],
],
'appearance' => [
'label' => __('site.menu_item_6'),
'items' => [
'themes' => ['label' => __('general.perm_themes'), 'icon' => 'fa-table'],
'customize' => ['label' => __('general.perm_customize'), 'icon' => 'fa-paintbrush'],
'share' => ['label' => __('general.perm_share'), 'icon' => 'fa-share-from-square'],
'bg' => ['label' => __('general.perm_background'), 'icon' => 'fa-image'],
'icon' => ['label' => __('general.perm_icon'), 'icon' => 'fa-earth-americas'],
'players' => ['label' => __('general.perm_player'), 'icon' => 'fa-circle-play'],
],
],
],
],
'radio' => [
'label' => __('general.header_nav_radio'),
'icon' => 'fa-tower-broadcast',
'prefix' => 'radio',
'sections' => [
'control' => [
'label' => __('general.perm_section_control'),
'items' => [
'home' => ['label' => __('general.perm_home'), 'icon' => 'fa-house'],
'playlists' => ['label' => __('general.perm_playlists'), 'icon' => 'fa-bars-staggered'],
'schedules' => ['label' => __('general.perm_schedules'), 'icon' => 'fa-clock'],
'library' => ['label' => __('general.perm_library'), 'icon' => 'fa-compact-disc'],
'file-manager' => ['label' => __('general.perm_files'), 'icon' => 'fa-folder-open'],
'live-passwords' => ['label' => __('general.perm_djpasswords'), 'icon' => 'fa-key'],
'relays' => ['label' => __('general.perm_relays'), 'icon' => 'fa-share-nodes'],
'blacklist' => ['label' => __('general.perm_blacklist'), 'icon' => 'fa-ban'],
],
],
'tools' => [
'label' => __('general.perm_section_tools'),
'items' => [
'statistics' => ['label' => __('general.perm_statistics'), 'icon' => 'fa-chart-line'],
'reports' => ['label' => __('general.perm_reports'), 'icon' => 'fa-file-lines'],
'players' => ['label' => __('general.perm_players'), 'icon' => 'fa-circle-play'],
'widgets' => ['label' => __('general.perm_players_integrations'), 'icon' => 'fa-code'],
'apps' => ['label' => __('general.perm_apps'), 'icon' => 'fa-mobile-screen'],
'api' => ['label' => __('general.perm_api'), 'icon' => 'fa-plug'],
'sett' => ['label' => __('general.perm_settings'), 'icon' => 'fa-sliders'],
],
],
],
],
];
/* TV lives outside the panel: no route checks this one, the header button does */
/* stored as tv|home, the name DashController::manageSiteTv checks on the way back from the TV */
if ($hasTv) {
$groups['tv'] = [
'label' => __('general.perm_group_tv'),
'icon' => 'fa-video',
'prefix' => 'tv',
'sections' => [
'main' => [
'label' => __('general.perm_group_tv'),
'items' => [
'home' => ['label' => __('general.perm_tv_access'), 'icon' => 'fa-tv'],
],
],
],
];
}
/* without a radio stream there is no radio area and no studio camera to allow */
if (! $hasStream) {
unset($groups['radio']);
unset($groups['site']['sections']['interactivity']['items']['camera']);
}
return $groups;
}
}
if (! function_exists('can_open_tv')) {
/**
* Whether this person may open the site's TV: the owner always, anyone
* else only as an active administrator holding the TV permission on this
* very site. The header button and the way back from the TV both ask here.
*/
function can_open_tv($site, $userId, $serverId)
{
if ((int) $site->user_id === (int) $userId) {
return true;
}
/* tv|home: the name the TV card of permission_groups stores */
return app(App\Services\SitePermissions::class)
->has('tv|home', (int) $site->id, (int) $userId, (int) $serverId);
}
}
if (! function_exists('permission_name')) {
/**
* The name a permission is stored under.
*
* Same string the middleware assembles from the route name, which is why
* the prefix comes from the group and not from a table apart.
*/
function permission_name($groups, $groupKey, $item)
{
$prefix = $groups[$groupKey]['prefix'] ?? $groupKey;
return $prefix === '' ? $item : $prefix . '|' . $item;
}
}
if (! function_exists('video_thumb')) {
/**
* Cover of a video, by the kind of address it came from.
*
* YouTube keeps only the id, Vimeo answers with the whole address, and an
* embed has no cover at all.
*/
function video_thumb($type, $thumbnail)
{
$thumbnail = (string) $thumbnail;
if ($thumbnail === '' || $thumbnail === '0') {
return null;
}
if ((string) $type === '1') {
return 'https://i.ytimg.com/vi/' . $thumbnail . '/hqdefault.jpg';
}
return str_starts_with($thumbnail, 'http') ? $thumbnail : null;
}
}
if (! function_exists('radio_subnav_groups')) {
/**
* Grupos do submenu da radio, com a pagina aberta ja marcada.
*
* Mesma forma do submenu do site, com uma diferenca: aqui ha entradas
* soltas, sem lista, porque o menu antigo tratava Estatisticas, Transmissao
* e Configuracoes como link direto. Grupo sem `items` vira link.
*/
function radio_subnav_groups()
{
$site = site_row();
if (! $site) {
return [];
}
$current = (string) Route::currentRouteName();
$grupos = [
[
'label' => __('radio.menu_item_1'),
'icon' => 'fa-house',
'route' => 'radio.home.r',
],
[
'label' => __('radio.menu_item_2'),
'icon' => 'fa-compact-disc',
'items' => [
['label' => __('radio.menu_item_2_sub_1'), 'icon' => 'fa-folder-open', 'route' => 'radio.file-manager.r'],
['label' => __('radio.menu_item_2_sub_2'), 'icon' => 'fa-list', 'route' => 'radio.playlists.r'],
['label' => __('radio.menu_item_2_sub_3'), 'icon' => 'fa-calendar-week', 'route' => 'radio.schedules.r'],
],
],
[
'label' => __('radio.menu_item_3'),
'icon' => 'fa-chart-line',
'route' => 'radio.statistics.r',
],
[
'label' => __('radio.menu_item_4'),
'icon' => 'fa-lock',
'route' => 'radio.live-passwords.r',
],
[
'label' => __('radio.menu_item_5'),
'icon' => 'fa-screwdriver-wrench',
'items' => [
['label' => __('radio.menu_item_5_sub_1'), 'icon' => 'fa-tower-broadcast', 'route' => 'radio.relays.r'],
['label' => __('radio.menu_item_5_sub_3'), 'icon' => 'fa-file-contract', 'route' => 'radio.historyAll'],
['label' => __('radio.menu_item_5_sub_5'), 'icon' => 'fa-code', 'route' => 'radio.api-integration.r'],
['label' => __('radio.menu_item_5_sub_6'), 'icon' => 'fa-box-open', 'route' => 'radio.library.r'],
['label' => __('radio.menu_item_5_sub_7'), 'icon' => 'fa-users-slash', 'route' => 'radio.blacklist.r'],
],
],
[
'label' => __('radio.menu_item_6'),
'icon' => 'fa-gear',
'route' => 'radio.sett.r',
],
];
$marcado = static function (array $entrada) use ($current) {
$bases = $entrada['match'] ?? [preg_replace('/\.(r|u)$/', '', $entrada['route'])];
foreach ($bases as $base) {
if ($current === $base || str_starts_with($current, $base . '.')) {
return true;
}
}
return false;
};
$out = [];
foreach ($grupos as $grupo) {
/* entrada solta: sem lista, vira link direto */
if (empty($grupo['items'])) {
if (! Route::has($grupo['route'])) {
continue;
}
$out[] = [
'label' => $grupo['label'],
'icon' => $grupo['icon'],
'url' => route($grupo['route']),
'active' => $marcado($grupo),
'items' => [],
];
continue;
}
$items = [];
$ativo = false;
foreach ($grupo['items'] as $item) {
if (! Route::has($item['route'])) {
continue;
}
$on = $marcado($item);
$ativo = $ativo || $on;
$items[] = [
'label' => $item['label'],
'icon' => $item['icon'],
'url' => route($item['route']),
'active' => $on,
];
}
if (! $items) {
continue;
}
$out[] = [
'label' => $grupo['label'],
'icon' => $grupo['icon'],
'url' => null,
'active' => $ativo,
'items' => $items,
];
}
return $out;
}
}
if (! function_exists('log_action_label')) {
/**
* Reads an action slug into a sentence: entity.verb, with an optional
* bulk tail. An action with no wording yet falls back to its own slug,
* so a new one never shows up as an empty line.
*/
function log_action_label($action)
{
$parts = explode('.', (string) $action);
$bulk = end($parts) === 'bulk';
if ($bulk) {
array_pop($parts);
}
$verb = array_pop($parts);
$entity = implode('_', $parts);
$verbKey = 'general.log_verb_' . $verb;
$entityKey = 'general.log_entity_' . $entity;
if (! $entity || __($verbKey) === $verbKey || __($entityKey) === $entityKey) {
return (string) $action;
}
$line = __('general.log_action_line', ['verb' => __($verbKey), 'entity' => __($entityKey)]);
return $bulk ? __('general.log_action_bulk', ['line' => $line]) : $line;
}
}
if (! function_exists('log_action_icon')) {
/** Icon for an action, read from its verb: the entity varies, the verb does not. */
function log_action_icon($action)
{
$parts = explode('.', (string) $action);
if (end($parts) === 'bulk') {
array_pop($parts);
}
$icons = [
'create' => 'fa-plus',
'update' => 'fa-pen',
'delete' => 'fa-trash',
'change' => 'fa-arrows-rotate',
'toggle' => 'fa-toggle-on',
'raffle' => 'fa-gift',
'clear' => 'fa-eraser',
'copy' => 'fa-copy',
'reset' => 'fa-rotate-left',
'permissions' => 'fa-key',
'password' => 'fa-key',
'invite' => 'fa-paper-plane',
];
return $icons[end($parts)] ?? 'fa-clock-rotate-left';
}
}
if (! function_exists('same_ip')) {
/**
* Whether two addresses are the same machine.
*
* The panel and the streaming server write the address in their own way:
* one may arrive as IPv4 mapped into IPv6 (::ffff:1.2.3.4), the other as a
* shortened IPv6. Comparing the packed bytes settles every spelling.
*/
function same_ip($one, $other)
{
$one = trim((string) $one);
$other = trim((string) $other);
if ($one === '' || $other === '') {
return false;
}
if (strcasecmp($one, $other) === 0) {
return true;
}
$bytes = static function ($ip) {
/* an IPv4 wearing an IPv6 coat is still that IPv4 */
if (stripos($ip, '::ffff:') === 0) {
$ip = substr($ip, 7);
}
$packed = @inet_pton($ip);
return $packed === false ? null : $packed;
};
$a = $bytes($one);
$b = $bytes($other);
return $a !== null && $b !== null && $a === $b;
}
}