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 &#xNN; 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('&#x', '', $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', '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); $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('setReport')) { function setReport($data){ $datetime = new \DateTime(); $timezone = new \DateTimeZone((isset($data->timezone) ? $data->timezone : 'America/Sao_Paulo')); $datetime->setTimezone($timezone); $finalDate = $datetime->format('Y-m-d H:i:s'); $report = new \stdClass(); $report->port = $data->port; $report->type = (isset($data->type) ? $data->type : '1'); $report->file = $data->file; $report->created_at = $finalDate; ProcessReports::dispatch($report); return true; } } 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(); $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('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)], ['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; } $bases = $item['match'] ?? [preg_replace('/\.(r|u)$/', '', $item['route'])]; $on = false; foreach ($bases as $base) { /* o ponto no fim evita que site.posts marque site.postsAlgo */ if ($current === $base || str_starts_with($current, $base . '.')) { $on = true; break; } } $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'], ]; $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) { $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-list-music'], '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'], ], ], ], ], ]; if ($hasTv) { $groups['radio']['sections']['control']['items']['tv'] = ['label' => __('general.perm_tv_access'), 'icon' => 'fa-tv']; } return $groups; } } 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.reports.r'], ['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; } }