Refector code

This commit is contained in:
iamdoubz
2026-07-24 05:34:07 -05:00
parent 6ab627ba60
commit 2fbbf4b0fc
23 changed files with 1518 additions and 1069 deletions
+147
View File
@@ -0,0 +1,147 @@
# =============================================================================
# Radio station configuration
# ---------------------------------------------------------------------------
# Copy this file to ".env" and edit the values. Every station only needs to
# touch this file — no PHP changes required.
#
# cp .env.example .env
#
# Booleans accept: true/false, 1/0, yes/no, on/off.
# Lines starting with # are comments. Quote values that contain spaces or #.
# =============================================================================
# -----------------------------------------------------------------------------
# Branding & metadata
# -----------------------------------------------------------------------------
SITE_NAME="My Radio"
# Short name used for og:site_name (defaults to SITE_NAME).
SITE_SHORT_NAME="My Radio"
SITE_TAGLINE="Streaming 24/7"
SITE_DESCRIPTION="Listen to my internet radio station 24/7 with all the music you can handle!"
SITE_KEYWORDS="radio, internet radio, music, live stream"
# Public URL of this site, with trailing slash. Used for canonical/OpenGraph tags.
SITE_URL="https://radio.example.com/"
SITE_LOCALE="en_US"
SITE_LANG="en"
SITE_AUTHOR=""
# -----------------------------------------------------------------------------
# SEO / icons
# -----------------------------------------------------------------------------
# OpenGraph / share image. Absolute URL recommended (falls back to album art
# at runtime once a track is playing).
OG_IMAGE="https://radio.example.com/assets/icons/og.png"
# Set true if you have dropped a full favicon set into ICONS_DIR
# (apple-touch-icon.png, favicon-32x32.png, favicon-16x16.png, site.webmanifest).
SHOW_ICONS=false
ICONS_DIR="assets/icons"
# Browser UI colors.
THEME_COLOR="#e21d31"
TILE_COLOR="#5e656e"
# -----------------------------------------------------------------------------
# Stream (the audio the browser plays)
# -----------------------------------------------------------------------------
# Public URL of your stream mount (what nginx proxies to Icecast).
STREAM_URL="https://radio.example.com/stream"
STREAM_TYPE="audio/mpeg"
# -----------------------------------------------------------------------------
# Now playing (metadata the SERVER fetches, then normalizes for the browser)
# -----------------------------------------------------------------------------
# provider: icecast | azuracast | shoutcast | custom
NOWPLAYING_PROVIDER="icecast"
# The status endpoint your PHP server fetches. This may be an internal address
# (e.g. http://127.0.0.1:8000/...) since only the server hits it.
# icecast -> http://127.0.0.1:8000/status-json.xsl
# azuracast -> https://azura.example.com/api/nowplaying/your_station_shortcode
# shoutcast -> http://127.0.0.1:8000/stats?json=1
# custom -> any URL returning JSON
NOWPLAYING_URL="http://127.0.0.1:8000/status-json.xsl"
# Icecast only: if you run multiple mounts, name the one to read (e.g. /stream).
ICECAST_MOUNT=""
# How to split a combined "Artist<delim>Title" string (Icecast/Shoutcast).
# The original dou.bet station uses " <-|-> ". Most setups use " - ".
TITLE_DELIMITER=" - "
# Polling / timeouts (seconds).
NOWPLAYING_POLL_SECONDS=10
NOWPLAYING_TIMEOUT=5
# --- custom provider only: JSON field paths (dot notation, arrays ok: a.b.0.c)
CUSTOM_ARTIST_PATH=""
CUSTOM_TITLE_PATH=""
CUSTOM_ALBUM_PATH=""
CUSTOM_ART_PATH=""
CUSTOM_LISTENERS_PATH=""
# If your feed only has a single combined field, set this instead of
# CUSTOM_ARTIST_PATH/CUSTOM_TITLE_PATH; it is split on TITLE_DELIMITER.
CUSTOM_TITLE_COMBINED_PATH=""
# -----------------------------------------------------------------------------
# Artwork & lyrics (Genius is optional)
# -----------------------------------------------------------------------------
# source: auto | provider | genius | none
# auto = use provider art if present, else Genius (if configured), else fallback
# provider = only use art the stream provider itself supplies (e.g. AzuraCast)
# genius = always look art up on Genius
# none = always use the fallback image
ARTWORK_SOURCE="auto"
FALLBACK_ART="assets/img/placeholder.svg"
# Show a "Show lyrics" button (requires a Genius token below).
LYRICS_ENABLED=true
# Genius API token. Get one at https://genius.com/api-clients (Client Access Token).
# Provide EITHER the plain token...
GENIUS_TOKEN=""
# ...OR a base64-encoded token (kept for compatibility with the original project).
GENIUS_API_BASE64=""
# -----------------------------------------------------------------------------
# Theme & colors (also drives CSS variables — restyle without editing CSS)
# -----------------------------------------------------------------------------
THEME_BG="#000000"
THEME_BG_MOBILE="#262626"
THEME_SURFACE="#1a1a1a"
THEME_TEXT="#ffffff"
THEME_MUTED="#bfbfbf"
THEME_ACCENT="#e21d31"
THEME_ACCENT_HOVER="#cf1b2d"
THEME_ACCENT_TEXT="#000000"
THEME_RADIUS="15px"
THEME_FONT="Helvetica, Calibri, Tahoma, Verdana, Arial, sans-serif"
# -----------------------------------------------------------------------------
# Listeners / history
# -----------------------------------------------------------------------------
SHOW_LISTENERS=true
# Optional: wrap the listener count in a link (e.g. a directory listing).
LISTENERS_LINK=""
# Show the slide-out "recently played" history table.
SHOW_HISTORY=true
# -----------------------------------------------------------------------------
# Analytics (optional — leave blank to disable)
# -----------------------------------------------------------------------------
MATOMO_URL=""
MATOMO_SITE_ID=""
# -----------------------------------------------------------------------------
# Runtime
# -----------------------------------------------------------------------------
TIMEZONE="UTC"
DEBUG=false
SHOW_ERRORS=false
+132 -138
View File
@@ -1,158 +1,152 @@
# Radio
This is my attempt at making a repo for my radio website found at https://radio.dou.bet. It needs a lot of work to be more "generic" for others to use, but it is a start nonetheless.
A small, self-hostable website for an internet radio station. Point it at your
stream, edit one `.env` file, and you get a live player with album art, a
"now playing" title, a recently-played history, listener counts, and optional
lyrics — no code changes required.
## WARNING
Originally built for [radio.dou.bet](https://radio.dou.bet); this version is
generic so anyone can drop in their own configuration.
This code isn't production ready. My website is hard coded in most files. There is no standard for coding. I created this to tell myself "Yes you can stream an internet radio station from home".
## Features
- Live broadcast using [icecast](https://icecast.org/)
- Detailed listening history
- DJing software provided by [mixxx](https://mixxx.org/)
- Artwork and Lyrics using [Genius API](https://docs.genius.com/)
- Live polling of stream count
## Prerequisites
- You have already compiled and installed icecast
- You have downloaded and installed mixxx
- You have configured icecast and pointed it to your server name i.e. radio.my.domain
- You have added your icecast server to the Live Broadcasting source connection in mixxx
- You have music you want to stream on the internet
## Install
This section will eventually be populated with what you should do. For now, it is just `cd /var/www && sudo -u www-data git pull https://git.dou.bet/iamdoubz/radio radio` and then add a virtual host for your favorite web frontend. NOTE: my website is hard coded in most of these files. Each file will need to be checked to point to your actual radio streaming website.
## Pix
| [![Arc AtfdsjDppb](https://pix.dou.bet/images/2025/04/11/Arc_AtfdsjDppb.md.png)](https://pix.dou.bet/image/Arc-AtfdsjDppb.gZNv) |
| :---: |
| [![IMG 0050](https://pix.dou.bet/images/2025/04/11/IMG_0050.md.jpg)](https://pix.dou.bet/image/IMG-0050.gjZf) |
## nginx example
This isn't a complete example, but the overall base should be covered.
## How it works
```
server {
server_name radio.my.domain;
root /var/www/radio;
include defaults-http.conf;
}
server {
### Default config ###
server_name radio.my.domain;
include defaults-https.conf;
### Block HTTP/1.0 traffic
if ($server_protocol = HTTP/1.0) {
return 444;
}
### Performance tuning config ###
client_max_body_size 16M;
client_body_timeout 300s;
client_body_buffer_size 256k;
### Default headers ###
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "noindex, nofollow" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=15768000" always;
add_header Permissions-Policy "browsing-topics=(self), geolocation=(self), midi=(self), sync-xhr=(self), microphone=(self), camera=(self), magnetometer=(self), gyroscope=(self), fullscreen=(self), payment=(self), interest-cohort=()";
### Proxy php rules ###
index index.php index.html /index.php$request_uri;
fastcgi_buffers 64 4K;
fastcgi_hide_header X-Powered-By;
proxy_intercept_errors off;
browser ──▶ index.php / assets/* (the page + player)
browser ──▶ api/nowplaying.php ──▶ Icecast / AzuraCast / Shoutcast / custom
browser ──▶ api/artwork.php ──▶ Genius API (optional)
browser ──▶ api/lyrics.php ──▶ Genius song page (optional)
```
root /var/www/radio;
access_log /var/log/nginx/radio_access.log;
error_log /var/log/nginx/radio_error.log info;
The browser only ever talks to *this* site. PHP fetches your stream backend
server-side and returns one normalized JSON shape, which means no CORS headaches
and your internal backend address (e.g. `127.0.0.1:8000` behind nginx) is never
exposed to visitors. A typical deployment is **nginx → PHP-FPM** on the web box,
with nginx also proxying the audio stream from Icecast.
location = /peers.xsl {
access_log off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:10038/status-json.xsl;
proxy_redirect off;
proxy_ssl_verify off;
proxy_http_version 1.1;
}
location = /status-json.xsl {
access_log off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:10038;
proxy_redirect off;
proxy_ssl_verify off;
proxy_http_version 1.1;
}
## Requirements
- PHP 7.4+ (8.x recommended) with cURL, served by any web server (nginx + PHP-FPM, Apache, Caddy…)
- A running stream with a now-playing endpoint — Icecast, AzuraCast, Shoutcast, or any JSON feed
- *(optional)* a free [Genius API token](https://genius.com/api-clients) for album art and lyrics
## Quick start
```bash
git clone https://git.dou.bet/iamdoubz/radio.git
cd radio
cp .env.example .env
# edit .env — at minimum set SITE_NAME, STREAM_URL, and NOWPLAYING_URL
php -S localhost:8000 # for a quick local test
```
Then open <http://localhost:8000>. For production, serve the folder with
nginx/Apache and PHP-FPM (see the example config near the bottom).
## Configuration
Everything lives in `.env` — see [`.env.example`](.env.example) for the fully
documented list. The main groups are:
- **Branding & metadata** — `SITE_NAME`, `SITE_DESCRIPTION`, `SITE_KEYWORDS`, `SITE_URL`, `OG_IMAGE`.
- **Stream** — `STREAM_URL` (what the browser plays) and `STREAM_TYPE`.
- **Now playing** — `NOWPLAYING_PROVIDER`, `NOWPLAYING_URL`, and provider-specific keys (below).
- **Artwork & lyrics** — `ARTWORK_SOURCE`, `LYRICS_ENABLED`, `GENIUS_TOKEN`.
- **Theme & colors** — `THEME_BG`, `THEME_ACCENT`, `THEME_TEXT`, `THEME_FONT`, … (drive CSS variables directly).
### Now-playing providers
Set `NOWPLAYING_PROVIDER` and `NOWPLAYING_URL`:
| Provider | `NOWPLAYING_URL` example | Notes |
|-------------|--------------------------|-------|
| `icecast` | `http://127.0.0.1:8000/status-json.xsl` | Set `ICECAST_MOUNT` if you run several mounts. Splits the stream title on `TITLE_DELIMITER`. |
| `azuracast` | `https://azura.example.com/api/nowplaying/your_station` | Richest option — provides artist, title, album, **art**, and listeners natively (no Genius needed). |
| `shoutcast` | `http://127.0.0.1:8000/stats?json=1` | Splits the stream title on `TITLE_DELIMITER`. |
| `custom` | any URL returning JSON | Map fields with `CUSTOM_ARTIST_PATH`, `CUSTOM_TITLE_PATH`, `CUSTOM_ART_PATH`, etc. (dot notation, e.g. `data.track.0.artist`). |
`TITLE_DELIMITER` is how a combined `Artist – Title` string is split. The
original dou.bet station uses `" <-|-> "`; most setups use `" - "`.
### Artwork & lyrics
`ARTWORK_SOURCE` controls where cover art comes from:
- `auto` — use the art your provider supplies (e.g. AzuraCast); otherwise look it up on Genius; otherwise show the placeholder.
- `provider` — only ever use provider art.
- `genius` — always look art up on Genius.
- `none` — always show `FALLBACK_ART`.
Lyrics require `LYRICS_ENABLED=true` **and** a Genius token. Lyrics are scraped
from the public Genius song page and returned as plain text; if Genius changes
their page layout, `api/lyrics.php` may need a tweak.
You can hard-code artwork for specific tracks by creating `config/overrides.json`:
```json
{ "artwork": [ { "artist": "Steve Reich", "art": "https://example.com/reich.jpg" } ] }
```
### Theming
Colors and fonts are exposed as CSS variables generated from your `.env`, so you
can re-skin the site without editing CSS. For deeper layout changes edit
[`assets/css/radio.css`](assets/css/radio.css), which uses those variables
(`var(--accent)`, `var(--bg)`, …).
## Project structure
```
config.php # loads .env, applies defaults, exposes cfg()/config()
index.php # the page
partials/head.php # <head>: meta, SEO, theme variables, analytics
partials/noscript.php # no-JavaScript fallback
api/_bootstrap.php # shared helpers (config, HTTP, JSON)
api/nowplaying.php # normalizes Icecast/AzuraCast/Shoutcast/custom
api/artwork.php # optional Genius album-art lookup
api/lyrics.php # optional Genius lyrics scraper
assets/css/radio.css # themeable styles
assets/js/player.js # vanilla-JS player (no jQuery)
assets/img/ # placeholder art
.env.example # every setting, documented
```
## nginx + PHP-FPM example
```nginx
server {
server_name radio.example.com;
root /var/www/radio;
index index.php;
# Proxy the audio stream from Icecast (keeps Icecast off the public net).
location = /stream {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000/your_mount;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:10038;
proxy_redirect off;
proxy_ssl_verify off;
proxy_http_version 1.1;
}
location = /peers2.xsl {
access_log off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://192.168.1.24:10036/status-json.xsl;
proxy_redirect off;
proxy_ssl_verify off;
proxy_http_version 1.1;
}
location = /status-json2.xsl {
access_log off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://192.168.1.24:10036/status-json.xsl;
proxy_redirect off;
proxy_ssl_verify off;
proxy_http_version 1.1;
}
location = /stream2 {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://192.168.1.24:10036;
proxy_redirect off;
proxy_ssl_verify off;
proxy_http_version 1.1;
}
location / { try_files $uri $uri/ /index.php$is_args$args; }
location ~ \.php$ {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param HTTPS on;
fastcgi_param modHeadersAvailable true;
fastcgi_param front_controller_active true;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_max_temp_file_size 0;
fastcgi_index index.php;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
```
With the server-side `api/nowplaying.php` proxy you no longer need the old
`status-json.xsl` / `peers.xsl` nginx location blocks — PHP fetches the backend
directly via `NOWPLAYING_URL`.
## Security notes
- `.env` is gitignored; keep your Genius token there, never in committed code.
- Set `SHOW_ERRORS=false` and `DEBUG=false` in production.
- The lyrics/artwork endpoints echo back only sanitized data (plain-text lyrics, provider URLs).
## Credits
Broadcast with [Icecast](https://icecast.org/) and
[Mixxx](https://mixxx.org/); artwork and lyrics via the
[Genius API](https://docs.genius.com/). Licensed under the terms in
[LICENSE](LICENSE).
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* api/_bootstrap.php
* ------------------
* Shared setup for every endpoint under /api: loads config, applies error
* settings, and provides small HTTP / JSON helpers.
*/
require_once __DIR__ . '/../config.php';
if (cfg('runtime.show_errors')) {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
} else {
error_reporting(0);
}
/** Send a JSON response and stop. */
function json_out($data, $status = 200) {
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache, must-revalidate, max-age=0');
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
/** Plain-text response and stop (used by the lyrics endpoint). */
function text_out($text, $status = 200) {
http_response_code($status);
header('Content-Type: text/plain; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache, must-revalidate, max-age=0');
echo $text;
exit;
}
/**
* Perform an HTTP GET and return the body (or null on failure).
* Falls back to file_get_contents when cURL is unavailable.
*/
function http_get($url, $timeout = 5, $headers = []) {
if ($url === '') return null;
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => (int) $timeout,
CURLOPT_TIMEOUT => (int) $timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_USERAGENT => 'radio-site/1.0 (+https://github.com)',
]);
if (!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body === false || $code >= 400) return null;
return $body;
}
// Fallback without cURL.
$ctx = stream_context_create([
'http' => [
'timeout' => (int) $timeout,
'header' => implode("\r\n", $headers),
'ignore_errors' => true,
],
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
]);
$body = @file_get_contents($url, false, $ctx);
return ($body === false) ? null : $body;
}
/**
* Read a value out of a decoded-JSON structure using dot notation.
* Supports array indexes: "a.b.0.c".
*/
function dot_get($data, $path, $default = null) {
if ($path === '' || $path === null) return $default;
$node = $data;
foreach (explode('.', $path) as $key) {
if (is_array($node) && array_key_exists($key, $node)) {
$node = $node[$key];
} elseif (is_object($node) && isset($node->$key)) {
$node = $node->$key;
} else {
return $default;
}
}
return $node;
}
/** Read the artist/title the browser sends to artwork/lyrics endpoints. */
function request_track() {
$artist = $_POST['artist'] ?? $_GET['artist'] ?? '';
$title = $_POST['title'] ?? $_GET['title'] ?? '';
return [trim((string) $artist), trim((string) $title)];
}
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* api/artwork.php
* ---------------
* Optional album-art lookup via the Genius API. The browser calls this only
* when the stream provider itself did not supply artwork and ARTWORK_SOURCE
* allows Genius. Returns JSON:
*
* { "art": "https://…" | "", "album": "…", "url": "https://genius.com/…" }
*
* With no Genius token configured it simply returns empty fields, and the
* front-end falls back to the configured placeholder image.
*/
require_once __DIR__ . '/_bootstrap.php';
$empty = ['art' => '', 'album' => '', 'url' => ''];
$token = cfg('genius.token');
$source = cfg('artwork.source');
if ($token === '' || $source === 'none' || $source === 'provider') {
json_out($empty);
}
[$artist, $title] = request_track();
if ($artist === '' && $title === '') {
json_out($empty);
}
/* -- Operator-defined overrides (config/overrides.json) -------------------- */
$overridesFile = __DIR__ . '/../config/overrides.json';
if (is_file($overridesFile)) {
$ov = json_decode((string) file_get_contents($overridesFile), true);
foreach (($ov['artwork'] ?? []) as $rule) {
$ra = $rule['artist'] ?? null;
$rt = $rule['title'] ?? null;
$artistMatch = ($ra === null || strcasecmp($ra, $artist) === 0);
$titleMatch = ($rt === null || strcasecmp($rt, $title) === 0);
if ($artistMatch && $titleMatch && !empty($rule['art'])) {
json_out(['art' => $rule['art'], 'album' => ($rule['album'] ?? ''), 'url' => '']);
}
}
}
/* -- Genius lookup --------------------------------------------------------- */
$headers = ['Authorization: Bearer ' . $token];
$timeout = 5;
$query = rawurlencode(trim($artist . ' - ' . $title, ' -'));
$search = http_get('https://api.genius.com/search?q=' . $query, $timeout, $headers);
if ($search === null) json_out($empty);
$json = json_decode($search, true);
$hit = dot_get($json, 'response.hits.0.result', null);
if (!$hit) json_out($empty);
$out = [
'art' => (string) ($hit['header_image_url'] ?? $hit['song_art_image_url'] ?? ''),
'album' => '',
'url' => (string) ($hit['url'] ?? ''),
];
// Pull the album name (and better art) from the full song record.
if (!empty($hit['id'])) {
$songRaw = http_get('https://api.genius.com/songs/' . rawurlencode((string) $hit['id']), $timeout, $headers);
if ($songRaw !== null) {
$song = json_decode($songRaw, true);
$s = dot_get($song, 'response.song', null);
if ($s) {
$out['album'] = (string) dot_get($s, 'album.name', '');
$betterArt = (string) ($s['song_art_image_url'] ?? $s['header_image_url'] ?? '');
if ($betterArt !== '') $out['art'] = $betterArt;
}
}
}
// Treat Genius' generic default cover as "no art" so the placeholder shows.
if (strpos($out['art'], 'default_cover_image') !== false) {
$out['art'] = '';
}
json_out($out);
+112
View File
@@ -0,0 +1,112 @@
<?php
/**
* api/lyrics.php
* --------------
* Optional lyrics lookup. Finds the song on Genius, then scrapes the lyric
* containers from the public song page. Returns JSON:
*
* { "found": true|false, "lyrics": "plain text\nwith newlines", "url": "…" }
*
* Lyrics are returned as plain text (tags stripped) so the front-end can render
* them with textContent — no HTML injection from a third-party page.
*
* Note: this scrapes Genius' HTML, which can change at any time. If Genius
* restructures their page the extraction may need updating.
*/
require_once __DIR__ . '/_bootstrap.php';
$empty = ['found' => false, 'lyrics' => '', 'url' => ''];
if (!cfg('lyrics.enabled') || cfg('genius.token') === '') {
json_out($empty);
}
[$artist, $title] = request_track();
if ($artist === '' && $title === '') {
json_out($empty);
}
$token = cfg('genius.token');
$timeout = 5;
$headers = ['Authorization: Bearer ' . $token];
/**
* Return the inner HTML of a balanced <div> whose opening tag starts at $lt.
* Correctly handles nested <div> elements.
*/
function inner_balanced_div($html, $lt) {
$open = strpos($html, '>', $lt);
if ($open === false) return '';
$i = $open + 1;
$start = $i;
$len = strlen($html);
$depth = 1;
while ($i < $len && $depth > 0) {
$next = strpos($html, '<', $i);
if ($next === false) break;
if (strncasecmp(substr($html, $next, 5), '</div', 5) === 0) {
$depth--;
if ($depth === 0) return substr($html, $start, $next - $start);
$i = $next + 5;
} elseif (strncasecmp(substr($html, $next, 4), '<div', 4) === 0) {
$depth++;
$i = $next + 4;
} else {
$i = $next + 1;
}
}
return substr($html, $start, max(0, $i - $start));
}
/** Turn one container's HTML into plain text with newlines. */
function container_to_text($fragment) {
// Line breaks -> newlines before stripping tags.
$fragment = preg_replace('#<br\s*/?>#i', "\n", $fragment);
// Drop any stray script/style.
$fragment = preg_replace('#<(script|style)\b[^>]*>.*?</\1>#is', '', $fragment);
// Block-level tags imply a line break.
$fragment = preg_replace('#</(p|div)>#i', "\n", $fragment);
$text = strip_tags($fragment);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return $text;
}
/* -- Find the song on Genius ----------------------------------------------- */
$query = rawurlencode(trim($artist . ' - ' . $title, ' -'));
$search = http_get('https://api.genius.com/search?q=' . $query, $timeout, $headers);
if ($search === null) json_out($empty);
$json = json_decode($search, true);
$pageUrl = (string) dot_get($json, 'response.hits.0.result.url', '');
if ($pageUrl === '') json_out($empty);
/* -- Scrape the lyric containers ------------------------------------------- */
$page = http_get($pageUrl, $timeout);
if ($page === null) {
json_out(['found' => false, 'lyrics' => '', 'url' => $pageUrl]);
}
$marker = 'data-lyrics-container="true"';
$pieces = [];
$offset = 0;
while (($mpos = strpos($page, $marker, $offset)) !== false) {
// Backtrack to the start of this opening <div ...> tag.
$lt = strrpos(substr($page, 0, $mpos), '<div');
if ($lt === false) { $offset = $mpos + strlen($marker); continue; }
$inner = inner_balanced_div($page, $lt);
$text = trim(container_to_text($inner));
if ($text !== '') $pieces[] = $text;
$offset = $mpos + strlen($marker);
}
$lyrics = trim(implode("\n\n", $pieces));
// Trim stray indentation left on each line, then collapse blank-line runs.
$lyrics = implode("\n", array_map('trim', explode("\n", $lyrics)));
$lyrics = preg_replace("/\n{3,}/", "\n\n", $lyrics);
if ($lyrics === '') {
json_out(['found' => false, 'lyrics' => '', 'url' => $pageUrl]);
}
json_out(['found' => true, 'lyrics' => $lyrics, 'url' => $pageUrl]);
+150
View File
@@ -0,0 +1,150 @@
<?php
/**
* api/nowplaying.php
* ------------------
* Fetches the station's current metadata from whatever backend the operator
* uses (Icecast, AzuraCast, Shoutcast, or a custom JSON feed) and returns a
* single normalized shape the browser can rely on:
*
* {
* "online": true|false,
* "artist": "…",
* "title": "…",
* "album": "…",
* "art": "…" | "", // empty if the provider has no art
* "listeners": 12 | null,
* "listeners_peak": 34 | null
* }
*
* Doing this server-side avoids browser CORS problems and hides internal
* backend addresses (e.g. 127.0.0.1 / LAN IPs) from visitors.
*/
require_once __DIR__ . '/_bootstrap.php';
$provider = cfg('nowplaying.provider');
$url = cfg('nowplaying.url');
$timeout = cfg('nowplaying.timeout');
$delim = cfg('nowplaying.delimiter');
$out = [
'online' => false,
'artist' => '',
'title' => '',
'album' => '',
'art' => '',
'listeners' => null,
'listeners_peak' => null,
];
/** Split "Artist <delim> Title" into [artist, title]. */
function split_title($combined, $delim) {
$combined = trim((string) $combined);
if ($combined === '') return ['', ''];
if ($delim !== '' && strpos($combined, $delim) !== false) {
$parts = explode($delim, $combined, 2);
return [trim($parts[0]), trim($parts[1])];
}
// No delimiter found — treat the whole thing as the title.
return ['', $combined];
}
$raw = http_get($url, $timeout);
if ($raw === null) {
json_out($out); // backend unreachable -> offline
}
$data = json_decode($raw, true);
if (!is_array($data)) {
json_out($out);
}
switch ($provider) {
/* --------------------------------------------------------------- */
case 'azuracast':
$np = $data['now_playing'] ?? [];
$song = $np['song'] ?? [];
$out['artist'] = (string) ($song['artist'] ?? '');
$out['title'] = (string) ($song['title'] ?? '');
$out['album'] = (string) ($song['album'] ?? '');
$out['art'] = (string) ($song['art'] ?? '');
if ($out['artist'] === '' && $out['title'] === '' && !empty($song['text'])) {
[$out['artist'], $out['title']] = split_title($song['text'], $delim);
}
$out['listeners'] = isset($data['listeners']['current']) ? (int) $data['listeners']['current'] : null;
$out['listeners_peak'] = isset($data['listeners']['total']) ? (int) $data['listeners']['total'] : null;
$out['online'] = !empty($data['is_online']) && ($out['title'] !== '' || $out['artist'] !== '');
break;
/* --------------------------------------------------------------- */
case 'shoutcast':
$combined = $data['streamtitle'] ?? $data['songtitle'] ?? '';
[$out['artist'], $out['title']] = split_title($combined, $delim);
$out['listeners'] = isset($data['currentlisteners']) ? (int) $data['currentlisteners'] : null;
$out['listeners_peak'] = isset($data['peaklisteners']) ? (int) $data['peaklisteners'] : null;
$out['online'] = ($out['title'] !== '' || $out['artist'] !== '');
break;
/* --------------------------------------------------------------- */
case 'custom':
$c = cfg('nowplaying.custom');
if (!empty($c['title_combined'])) {
[$out['artist'], $out['title']] = split_title(dot_get($data, $c['title_combined'], ''), $delim);
} else {
$out['artist'] = (string) dot_get($data, $c['artist'], '');
$out['title'] = (string) dot_get($data, $c['title'], '');
}
$out['album'] = (string) dot_get($data, $c['album'], '');
$out['art'] = (string) dot_get($data, $c['art'], '');
$listeners = dot_get($data, $c['listeners'], null);
$out['listeners'] = ($listeners === null) ? null : (int) $listeners;
$out['online'] = ($out['title'] !== '' || $out['artist'] !== '');
break;
/* --------------------------------------------------------------- */
case 'icecast':
default:
$ice = $data['icestats'] ?? [];
$source = $ice['source'] ?? null;
if ($source === null) break;
// "source" can be a single object or a list of mounts.
$sources = isset($source[0]) ? $source : [$source];
$mount = cfg('nowplaying.mount');
$chosen = null;
if ($mount !== '') {
foreach ($sources as $s) {
$listenurl = (string) ($s['listenurl'] ?? '');
if ($listenurl !== '' && substr($listenurl, -strlen($mount)) === $mount) {
$chosen = $s;
break;
}
}
}
if ($chosen === null) {
// First source that actually has a title, else the first one.
foreach ($sources as $s) {
if (!empty($s['title']) || !empty($s['artist'])) { $chosen = $s; break; }
}
if ($chosen === null) $chosen = $sources[0];
}
// Liquidsoap can emit artist/title separately; prefer that.
if (!empty($chosen['artist']) || !empty($chosen['title'])) {
$maybeArtist = (string) ($chosen['artist'] ?? '');
$maybeTitle = (string) ($chosen['title'] ?? '');
if ($maybeArtist !== '' && $maybeTitle !== '') {
$out['artist'] = trim($maybeArtist);
$out['title'] = trim($maybeTitle);
} else {
[$out['artist'], $out['title']] = split_title($maybeTitle !== '' ? $maybeTitle : $maybeArtist, $delim);
}
}
$out['listeners'] = isset($chosen['listeners']) ? (int) $chosen['listeners'] : null;
$out['listeners_peak'] = isset($chosen['listener_peak']) ? (int) $chosen['listener_peak'] : null;
$out['online'] = ($out['title'] !== '' || $out['artist'] !== '');
break;
}
json_out($out);
+175
View File
@@ -0,0 +1,175 @@
/*
* radio.css — all colors/fonts come from CSS variables defined in <head>
* (see partials/head.php, fed by .env). Restyle a station without editing CSS.
*/
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--bg);
color: var(--text);
font-family: var(--font);
text-align: center;
-webkit-font-smoothing: antialiased;
}
a { color: var(--muted); text-decoration: none; }
a:hover, a:focus { color: var(--text); text-decoration: underline; }
/* ------------------------------------------------------------- layout */
.row {
display: flex;
justify-content: center;
width: 100%;
padding: 16px;
}
.column {
width: 100%;
max-width: 420px;
padding: 24px;
background: var(--surface);
border-radius: calc(var(--radius) + 10px);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
/* --------------------------------------------------------- album art */
.album-art {
width: 86%;
max-width: 100%;
height: auto;
border-radius: var(--radius);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
opacity: 0.85;
transition: opacity .2s ease-in-out, border-radius .2s ease-in-out;
}
.album-art:hover, .album-art:focus {
opacity: 1;
border-radius: 0;
}
/* --------------------------------------------------- now-playing text */
.np {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
line-height: 1.4;
min-height: 1.4em;
word-break: break-word;
}
/* --------------------------------------------------------- audio player */
.player {
width: 100%;
outline: 0;
}
audio[data-nostream] { display: none; }
/* -------------------------------------------------------------- buttons */
.btn-accent {
min-width: 120px;
padding: 9px 16px;
background: var(--accent);
color: var(--accent-text);
font-variant: small-caps;
font-size: 1rem;
border: 1px solid var(--accent);
border-radius: 6px;
cursor: pointer;
transition: background .15s ease, box-shadow .15s ease;
}
.btn-accent:hover, .btn-accent:focus {
background: var(--accent-hover);
border-color: var(--accent-hover);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
outline: 0;
}
/* --------------------------------------------------------------- lyrics */
.lyrics {
width: 100%;
max-height: 40vh;
overflow-y: auto;
text-align: left;
white-space: pre-line;
line-height: 1.5;
padding: 8px 4px;
color: var(--text);
}
/* ----------------------------------------------------- history panel */
.history-toggle {
position: fixed;
top: 16px;
right: 16px;
z-index: 30;
padding: 8px 14px;
background: var(--accent);
color: var(--accent-text);
font-variant: small-caps;
border: none;
border-radius: 8px;
cursor: pointer;
}
.history-toggle:hover, .history-toggle:focus { background: var(--accent-hover); outline: 0; }
.history {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(90vw, 420px);
padding: 64px 16px 16px;
background: var(--surface);
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
transform: translateX(100%);
transition: transform .35s ease;
overflow-y: auto;
z-index: 25;
}
.history.open { transform: translateX(0); }
.history-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
text-align: left;
}
.history-table th, .history-table td {
padding: 6px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
vertical-align: top;
}
.history-table th { color: var(--muted); font-weight: 600; }
/* ------------------------------------------------------------ listeners */
.listeners {
position: fixed;
bottom: 12px;
left: 12px;
font-size: 0.9rem;
color: var(--text);
opacity: 0.8;
}
/* ------------------------------------------------------------ responsive */
@media (max-width: 575px) {
body { background: var(--bg-mobile); }
.column { background: transparent; padding: 12px; }
.history { width: min(96vw, 420px); }
}
@media print {
html, body { display: none; }
}
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500" role="img" aria-label="No album art">
<rect width="500" height="500" fill="#1a1a1a"/>
<circle cx="250" cy="250" r="150" fill="none" stroke="#3a3a3a" stroke-width="4"/>
<circle cx="250" cy="250" r="46" fill="none" stroke="#3a3a3a" stroke-width="4"/>
<circle cx="250" cy="250" r="10" fill="#3a3a3a"/>
<text x="250" y="430" fill="#6a6a6a" font-family="Helvetica, Arial, sans-serif" font-size="26" text-anchor="middle">No album art</text>
</svg>

After

Width:  |  Height:  |  Size: 539 B

+195
View File
@@ -0,0 +1,195 @@
/*
* player.js — front-end for the radio site.
*
* Reads window.RADIO_CONFIG (injected by index.php) and talks only to our own
* /api endpoints, so it contains no station-specific URLs or strings.
*
* Responsibilities:
* - poll /api/nowplaying.php and show artist / title / album art
* - resolve artwork (provider art or Genius, per config)
* - fetch lyrics on demand (if enabled)
* - keep a "recently played" history list
* - show the live listener count
*/
(function () {
'use strict';
var CFG = window.RADIO_CONFIG || {};
var POLL_MS = Math.max(3, CFG.pollSeconds || 10) * 1000;
var $ = function (id) { return document.getElementById(id); };
var el = {
title: $('track-title'),
np: $('np'),
art: $('art'),
ogImage: $('metaimage1'),
lyricsToggle: $('lyrics-toggle'),
lyrics: $('lyrics'),
historyToggle:$('history-toggle'),
historyBody: $('history-body'),
history: $('history'),
listeners: $('listeners')
};
var currentKey = null; // artisttitle of the track on screen
var siteName = CFG.siteName || 'Radio';
/* ---------------------------------------------------------------- utils */
function trackKey(t) { return (t.artist || '') + '' + (t.title || ''); }
function label(t) {
if (t.artist && t.title) return t.artist + ' – ' + t.title;
return t.title || t.artist || '';
}
function fmtTime(d) {
function z(n) { return (n < 10 ? '0' : '') + n; }
return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds());
}
function setArt(url) {
var src = url || CFG.fallbackArt;
if (el.art && el.art.getAttribute('src') !== src) el.art.setAttribute('src', src);
if (el.ogImage && url) el.ogImage.setAttribute('content', url);
}
function api(path, opts) {
return fetch(path, opts).then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
});
}
/* ------------------------------------------------------------- artwork */
function resolveArtwork(track, providerArt) {
var source = CFG.artworkSource || 'auto';
if (source === 'none') { setArt(''); return; }
if (source === 'provider') { setArt(providerArt); return; }
if (source === 'auto' && providerArt) { setArt(providerArt); return; }
// source === 'genius', or 'auto' with no provider art -> ask our endpoint.
var body = new URLSearchParams();
body.set('artist', track.artist || '');
body.set('title', track.title || '');
api('api/artwork.php', { method: 'POST', body: body })
.then(function (res) {
setArt(res.art || providerArt || '');
if (res.album && el.art) el.art.title = 'From "' + res.album + '"';
})
.catch(function () { setArt(providerArt || ''); });
}
/* -------------------------------------------------------------- lyrics */
function loadLyrics(track) {
if (!CFG.lyricsEnabled || !el.lyricsToggle) return;
el.lyricsToggle.hidden = true;
el.lyricsToggle.textContent = 'Show lyrics';
if (el.lyrics) { el.lyrics.hidden = true; el.lyrics.textContent = ''; }
var body = new URLSearchParams();
body.set('artist', track.artist || '');
body.set('title', track.title || '');
api('api/lyrics.php', { method: 'POST', body: body })
.then(function (res) {
if (res.found && res.lyrics) {
if (el.lyrics) el.lyrics.textContent = res.lyrics;
el.lyricsToggle.hidden = false;
}
})
.catch(function () { /* leave hidden */ });
}
if (el.lyricsToggle) {
el.lyricsToggle.addEventListener('click', function () {
if (!el.lyrics) return;
var show = el.lyrics.hidden;
el.lyrics.hidden = !show;
el.lyricsToggle.textContent = show ? 'Hide lyrics' : 'Show lyrics';
});
}
/* ------------------------------------------------------------- history */
function addHistory(track) {
if (!el.historyBody) return;
var tr = document.createElement('tr');
[fmtTime(new Date()), track.artist || '', track.title || ''].forEach(function (v) {
var td = document.createElement('td');
td.textContent = v;
tr.appendChild(td);
});
el.historyBody.insertBefore(tr, el.historyBody.firstChild);
while (el.historyBody.children.length > 100) {
el.historyBody.removeChild(el.historyBody.lastChild);
}
}
if (el.historyToggle && el.history) {
el.historyToggle.addEventListener('click', function () {
var open = el.history.classList.toggle('open');
el.historyToggle.classList.toggle('open', open);
el.historyToggle.textContent = open ? 'Close' : 'Song list';
});
}
/* ----------------------------------------------------------- listeners */
function updateListeners(data) {
if (!el.listeners) return;
if (data.listeners == null) { el.listeners.textContent = ''; return; }
var text = String(data.listeners);
if (data.listeners_peak != null) text += ' / ' + data.listeners_peak;
if (CFG.listenersLink) {
var a = document.createElement('a');
a.href = CFG.listenersLink;
a.target = '_blank';
a.rel = 'noopener';
a.textContent = text;
el.listeners.textContent = '';
el.listeners.appendChild(a);
} else {
el.listeners.textContent = text;
}
}
/* ----------------------------------------------------------- analytics */
function track(artist, title) {
if (window._paq && artist && title) {
window._paq.push(['trackEvent', 'Song', artist, title]);
}
}
/* --------------------------------------------------------------- poll */
function onNewTrack(t, providerArt) {
var text = label(t);
if (el.np) el.np.textContent = text || siteName;
if (el.title) el.title.textContent = text ? (text + ' | ' + siteName) : siteName;
addHistory(t);
resolveArtwork(t, providerArt);
loadLyrics(t);
track(t.artist, t.title);
}
function goOffline() {
if (el.np) el.np.textContent = siteName;
if (el.title) el.title.textContent = siteName;
}
function poll() {
api('api/nowplaying.php')
.then(function (data) {
updateListeners(data);
if (!data.online) {
if (currentKey !== null) { currentKey = null; goOffline(); }
return;
}
var key = trackKey(data);
if (key !== currentKey && (data.artist || data.title)) {
currentKey = key;
onNewTrack(data, data.art || '');
}
})
.catch(function () { /* transient — try again next tick */ })
.then(function () { setTimeout(poll, POLL_MS); });
}
poll();
})();
+253
View File
@@ -0,0 +1,253 @@
<?php
/**
* config.php
* -----------
* Single source of truth for the whole site. Reads a local `.env` file and
* merges it over sensible defaults so the project works out of the box and can
* be fully re-skinned for any station without touching PHP.
*
* Nothing secret should live in this file — put secrets (Genius token, etc.)
* in `.env`, which is gitignored. See `.env.example` for every supported key.
*/
if (!defined('RADIO_CONFIG_LOADED')) {
define('RADIO_CONFIG_LOADED', true);
/* -------------------------------------------------------------------- */
/* Load .env */
/* -------------------------------------------------------------------- */
/**
* Minimal, predictable .env parser. We deliberately avoid parse_ini_file:
* its RAW scanner keeps the surrounding quotes, and its NORMAL scanner
* rewrites bare words like true/false/none — which would silently undo a
* user setting FEATURE=false. This parser just does: skip comments/blank
* lines and an optional [section] header, split on the first "=", strip one
* layer of matching quotes, and otherwise keep the value verbatim.
*/
function radio_parse_env($path) {
$out = [];
$lines = @file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) return $out;
foreach ($lines as $line) {
$trim = trim($line);
if ($trim === '' || $trim[0] === '#' || $trim[0] === ';') continue;
if ($trim[0] === '[' && substr($trim, -1) === ']') continue; // [section]
$eq = strpos($line, '=');
if ($eq === false) continue;
$key = trim(substr($line, 0, $eq));
if ($key === '') continue;
$val = ltrim(substr($line, $eq + 1));
if ($val !== '' && ($val[0] === '"' || $val[0] === "'")) {
$q = $val[0];
$end = strpos($val, $q, 1);
$val = ($end === false) ? substr($val, 1) : substr($val, 1, $end - 1);
} else {
// Strip an inline comment introduced by " #" or " ;".
foreach ([' #', ' ;'] as $c) {
$p = strpos($val, $c);
if ($p !== false) $val = substr($val, 0, $p);
}
$val = rtrim($val);
}
$out[$key] = $val;
}
return $out;
}
$GLOBALS['__RADIO_ENV'] = [];
$envPath = __DIR__ . '/.env';
if (is_file($envPath)) {
$GLOBALS['__RADIO_ENV'] = radio_parse_env($envPath);
}
/**
* Read a raw string value from the environment / .env.
*/
function radio_env_raw($key, $default = null) {
if (array_key_exists($key, $GLOBALS['__RADIO_ENV'])) {
return $GLOBALS['__RADIO_ENV'][$key];
}
$v = getenv($key);
return ($v === false) ? $default : $v;
}
/** Read a string, trimming surrounding whitespace. */
function radio_env($key, $default = '') {
$v = radio_env_raw($key, null);
if ($v === null || $v === '') return $default;
return trim($v);
}
/** Read a boolean. Accepts 1/true/yes/on (case-insensitive). */
function radio_env_bool($key, $default = false) {
$v = radio_env_raw($key, null);
if ($v === null || $v === '') return $default;
return in_array(strtolower(trim($v)), ['1', 'true', 'yes', 'on'], true);
}
/** Read an integer with a floor. */
function radio_env_int($key, $default = 0, $min = null) {
$v = radio_env_raw($key, null);
$n = ($v === null || $v === '') ? $default : (int) $v;
if ($min !== null && $n < $min) $n = $min;
return $n;
}
/* -------------------------------------------------------------------- */
/* Build the config array */
/* -------------------------------------------------------------------- */
$config = [
// ---- Branding & metadata --------------------------------------
'site' => [
'name' => radio_env('SITE_NAME', 'My Radio'),
'short_name' => radio_env('SITE_SHORT_NAME', radio_env('SITE_NAME', 'My Radio')),
'tagline' => radio_env('SITE_TAGLINE', 'Streaming 24/7'),
'description' => radio_env('SITE_DESCRIPTION', 'Listen to my internet radio station 24/7.'),
'keywords' => radio_env('SITE_KEYWORDS', 'radio, internet radio, music, live stream'),
'url' => rtrim(radio_env('SITE_URL', ''), '/') . '/',
'locale' => radio_env('SITE_LOCALE', 'en_US'),
'lang' => radio_env('SITE_LANG', 'en'),
'author' => radio_env('SITE_AUTHOR', ''),
],
// ---- SEO / icons ----------------------------------------------
'seo' => [
'og_image' => radio_env('OG_IMAGE', ''), // absolute URL or path
'icons_dir' => rtrim(radio_env('ICONS_DIR', 'assets/icons'), '/'),
'show_icons' => radio_env_bool('SHOW_ICONS', false),
'theme_color' => radio_env('THEME_COLOR', radio_env('THEME_ACCENT', '#e21d31')),
'tile_color' => radio_env('TILE_COLOR', radio_env('THEME_COLOR', '#5e656e')),
],
// ---- Stream & now-playing -------------------------------------
'stream' => [
'url' => radio_env('STREAM_URL', ''), // <audio> source
'type' => radio_env('STREAM_TYPE', 'audio/mpeg'),
],
'nowplaying' => [
// provider: icecast | azuracast | shoutcast | custom
'provider' => strtolower(radio_env('NOWPLAYING_PROVIDER', 'icecast')),
// URL the *server* fetches (may be an internal address).
'url' => radio_env('NOWPLAYING_URL', ''),
'mount' => radio_env('ICECAST_MOUNT', ''), // optional, picks a source
'delimiter' => radio_env_raw('TITLE_DELIMITER', ' - '), // splits "Artist<delim>Title"
'poll' => radio_env_int('NOWPLAYING_POLL_SECONDS', 10, 3),
'timeout' => radio_env_int('NOWPLAYING_TIMEOUT', 5, 1),
// custom-provider JSON field paths (dot notation, arrays allowed: a.b.0.c)
'custom' => [
'artist' => radio_env('CUSTOM_ARTIST_PATH', ''),
'title' => radio_env('CUSTOM_TITLE_PATH', ''),
'album' => radio_env('CUSTOM_ALBUM_PATH', ''),
'art' => radio_env('CUSTOM_ART_PATH', ''),
'listeners' => radio_env('CUSTOM_LISTENERS_PATH', ''),
// If you only have a combined "Artist - Title" field, set this
// instead of artist/title and it will be split on the delimiter.
'title_combined' => radio_env('CUSTOM_TITLE_COMBINED_PATH', ''),
],
],
// ---- Artwork & lyrics -----------------------------------------
'artwork' => [
// source: auto | provider | genius | none
// auto = provider art if present, else Genius (if configured), else fallback
// provider = only use art the stream provider gives us
// genius = always look art up on Genius
// none = always use the fallback image
'source' => strtolower(radio_env('ARTWORK_SOURCE', 'auto')),
'fallback' => radio_env('FALLBACK_ART', 'assets/img/placeholder.svg'),
],
'lyrics' => [
'enabled' => radio_env_bool('LYRICS_ENABLED', true),
],
'genius' => [
// Base64-encoded Genius bearer token (kept for compatibility with
// the original project). You can also set GENIUS_TOKEN as plain text.
'token' => (function () {
$b64 = radio_env('GENIUS_API_BASE64', '');
if ($b64 !== '') {
$decoded = base64_decode($b64, true);
if ($decoded !== false) return trim($decoded);
}
return radio_env('GENIUS_TOKEN', '');
})(),
],
// ---- Theme & colors -------------------------------------------
'theme' => [
'bg' => radio_env('THEME_BG', '#000000'),
'bg_mobile' => radio_env('THEME_BG_MOBILE', '#262626'),
'surface' => radio_env('THEME_SURFACE', '#1a1a1a'),
'text' => radio_env('THEME_TEXT', '#ffffff'),
'muted' => radio_env('THEME_MUTED', '#bfbfbf'),
'accent' => radio_env('THEME_ACCENT', '#e21d31'),
'accent_hover' => radio_env('THEME_ACCENT_HOVER', '#cf1b2d'),
'accent_text' => radio_env('THEME_ACCENT_TEXT', '#000000'),
'radius' => radio_env('THEME_RADIUS', '15px'),
'font' => radio_env('THEME_FONT', 'Helvetica, Calibri, Tahoma, Verdana, Arial, sans-serif'),
],
// ---- Listeners / extras ---------------------------------------
'features' => [
'show_listeners' => radio_env_bool('SHOW_LISTENERS', true),
'listeners_link' => radio_env('LISTENERS_LINK', ''), // optional external URL
'show_history' => radio_env_bool('SHOW_HISTORY', true),
],
// ---- Analytics (optional) -------------------------------------
'analytics' => [
'matomo_url' => radio_env('MATOMO_URL', ''),
'matomo_site_id' => radio_env('MATOMO_SITE_ID', ''),
],
// ---- Runtime --------------------------------------------------
'runtime' => [
'timezone' => radio_env('TIMEZONE', 'UTC'),
'debug' => radio_env_bool('DEBUG', false),
'show_errors' => radio_env_bool('SHOW_ERRORS', false),
],
];
$GLOBALS['__RADIO_CONFIG'] = $config;
date_default_timezone_set($config['runtime']['timezone']);
}
/** Return the whole config array. */
function config() {
return $GLOBALS['__RADIO_CONFIG'];
}
/** Dot-path getter into the config array: cfg('theme.accent'). */
function cfg($path, $default = null) {
$node = $GLOBALS['__RADIO_CONFIG'];
foreach (explode('.', $path) as $key) {
if (is_array($node) && array_key_exists($key, $node)) {
$node = $node[$key];
} else {
return $default;
}
}
return $node;
}
/** HTML-escape helper. */
function e($str) {
return htmlspecialchars((string) $str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* The subset of config that is safe and useful to expose to the browser.
* Emitted as `window.RADIO_CONFIG` by the page.
*/
function radio_client_config() {
return [
'pollSeconds' => cfg('nowplaying.poll'),
'artworkSource' => cfg('artwork.source'),
'fallbackArt' => cfg('artwork.fallback'),
'lyricsEnabled' => cfg('lyrics.enabled') && cfg('genius.token') !== '',
'showListeners' => cfg('features.show_listeners'),
'showHistory' => cfg('features.show_history'),
'listenersLink' => cfg('features.listeners_link'),
'siteName' => cfg('site.name'),
];
}
-107
View File
@@ -1,107 +0,0 @@
* {box-sizing: border-box;}
body {width: 100%; color: #fff; background-color: #000; text-align: center; font-family: Helvetica, Calibri, Tahoma, Verdana, Arial, sans-serif;}
a{text-decoration: none; color: #bfbfbf;}
a:hover{text-decoration: underline; color: #b3b3b3; cursor: pointer;}
a:visited, a:active, a:focus{text-decoration: underline; color: #ccc; cursor: pointer;}
/* https://www.w3schools.com/howto/howto_css_image_grid_responsive.asp */
.row {display:-ms-flexbox; display:flex; justify-content:center;
/*-ms-flex-wrap: wrap; flex-wrap:wrap;*/
max-width:100%; background-color:#000;}
.column {-ms-flex: 35%;/* IE10 */ flex: 35%; max-width: 35%; background-color: #1a1a1a; border: 2px solid #1a1a1a; border-radius: 25px;}
#dwdart{-ms-flex: 100%; flex: 100%; max-width: 100%; text-align: center; min-width: 100px;}
#genius{z-index: 6;text-align:center;position:relative;border-radius: 15px;box-shadow: 0 0 10px #000000;opacity: 0.7;transition: all .2s ease-in-out;-webkit-transition: all .2s ease-in-out;-moz-transition: all .2s ease-in-out;}
#genius:hover, #genius:active, #genius:focus{
opacity: 1; border-radius: 0; transition: all .2s ease-in-out; -webkit-transition: all .2s ease-in-out; -moz-transition: all .2s ease-in-out;
}
.coolbutton2{
width: 100px;
padding: 8px;
background: #000;
color: #e9e9e9;
font-variant: small-caps;
border: 1px solid #e9e9e9;
border-left: none;
cursor: pointer;
border-radius: 15px;
text-decoration: none;
outline:0;
}
.coolbutton{
width: 120px;
padding: 8px;
background: #e21d31;
color: #000000;
font-variant: small-caps;
border: 1px solid #e21d31;
border-left: none;
cursor: pointer;
border-radius: 5px;
text-decoration: none;
outline:0;
}
.coolbutton:active,.coolbutton:hover, .coolbutton:focus {
background: #CF1B2D; border: 1px solid #CF1B2D; outline: none; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); outline:0;
}
.hiddenl{display: none;}
.showl{display: block; color: #ffffff;}
#last_played{
position: fixed;
top: 23px;
right: -23px;
width: 90px;
height: 45px;
padding: 8px;
background: #e21d31;
color: #000000;
font-variant: small-caps;
border: 1px solid #e21d31;
border-left: none;
cursor: pointer;
border-bottom-left-radius:10px;
border-bottom-right-radius:10px;
text-decoration: none;
outline:0;
z-index: 7;
transform: rotate(90deg); -ms-transform: rotate(90deg); -moz-transform: rotate(90deg); -o-transform: rotate(90deg); -webkit-transform: rotate(90deg);
}
#last_played:hover{color: #ffffff;}
.hiddenl2{display: none;position: fixed; top: 0; right: 0; width: 0; background-color: #e21d31; color: #fff; text-align: left;z-index:7;}
.songlist{border:2px solid #000; box-shadow:none; width: 518px; table-layout: auto; white-space: normal!important;z-index:7;max-height:100%}
#stream{max-width:100%;text-decoration:none;outline:0}
#controls {width: 100%; text-align:center; float: center; padding: 25px; z-index: 99;}
#play, #pause{display:none; cursor:pointer; text-align:center; font-size:50px; /*background-color:#00ff00; border-radius:60px; border: 4px solid #000;*/ max-width:50px;}
#loading{text-align:center; font-size:40px;}
#peep{position: fixed; bottom: 0; right: 0; color: #ffffff;}
.fancyAudio{display:block; border-radius:20px; margin:0 auto; background-color:#000; color:#fff}
@media screen and (max-width: 1330px) {
.column {
-ms-flex: 50%;
flex: 50%;
max-width: 50%;
}
}
@media screen and (max-width: 575px) {
body{background-color: #262626;color: #ffffff}
a{text-decoration: none; color: #bfbfbf;}
a:hover{text-decoration: underline; color: #b3b3b3; cursor: pointer;}
a:visited{text-decoration: underline; color: #ccc; cursor: pointer;}
.row{background-color: #262626;}
.coolbutton{color: #ffffff}
.coolbutton2{background-color:#000;color:#fff;border:none}
.songlist{width: 400px;z-index:7;}
.column {
-ms-flex: 95%;
flex: 95%;
max-width: 95%;
text-align: center;
background-color: #262626;
border: none;
}
/*#peep{width: 100%; display: flex; position: fixed; right: 0; left: 0; justify-content: center; bottom: 16px; color: #fff;}*/
#peep{position:fixed;left:16px;bottom:16px;color:#fff;text-align:left}
}
@media print {
html, body {
display: none; /* hide whole page */
}
}
-4
View File
@@ -1,4 +0,0 @@
[env]
GENIUS_API_BASE64=
DEBUG=true
SHOW_ERRORS=true
-18
View File
@@ -1,18 +0,0 @@
<?php if (!defined('token') or !token) die ('This file cannot be directly accessed.<meta http-equiv="refresh" content="3;url=/">'); ?>
<!DOCTYPE html>
<html xml:lang="en" lang="en" dir="ltr" prefix="og: http://ogp.me/ns#">
<head>
<style>body{background: #000; background-color: #000}</style>
<noscript><?php require 'ns/h20a.php';?></noscript>
<?php require 'header.php';?>
<!-- Start external requirements -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"/>
<!-- End external requirements -->
<!-- Start homemade requirements -->
<link rel="stylesheet" type="text/css" href="css/radio.css"/>
<!-- End homemade requirements -->
</head>
-22
View File
@@ -1,22 +0,0 @@
<title id="track-title">Dou.Bet Radio</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="description" content="Listen to Doubet Radio 24/7 with all the pop, rock, and electronic music songs you can handle!"/>
<meta name="keywords" content="radio, internet radio, dou.bet, coding, blog, hobby, hobbies"/>
<meta name="viewport" content="width=device-width, initial-scale=0.75, maximum-scale=1.0"/>
<meta name="msapplication-TileColor" content="#2d89ef"/>
<meta name="theme-color" content="#ffffff"/>
<meta content="https://radio.dou.bet/seo/android-chrome-512x512.png" id="metaimage2">
<meta property="og:title" content="Dou.Bet Radio"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://radio.dou.bet/"/>
<meta property="og:image" content="https://radio.dou.bet/seo/ddfb.png" id="metaimage1"/>
<meta property="og:description" content="Listen to Doubet Radio 24/7 with all the pop, rock, and electronic music songs you can handle!"/>
<meta property="og:site_name" content="Dou.Bet"/>
<meta property="og:audio" content="http://radio.dou.bet/stream" />
<meta property="og:audio:secure_url" content="https://radio.dou.bet/stream" />
<meta property="og:audio:type" content="audio/mpeg" />
<link id="metaimage3" rel="apple-touch-icon" sizes="180x180" href="seo/apple-touch-icon.png"/>
<link id="metaimage4" rel="icon" type="image/png" sizes="32x32" href="seo/favicon-32x32.png"/>
<link id="metaimage5" rel="icon" type="image/png" sizes="16x16" href="seo/favicon-16x16.png"/>
<link rel="manifest" href="seo/site.webmanifest"/>
<link rel="mask-icon" href="seo/safari-pinned-tab.svg" color="#666666"/>
+58 -21
View File
@@ -1,24 +1,61 @@
<?php date_default_timezone_set('America/Chicago'); define('token', 'index'); require 'h.php'; ?>
<body>
<noscript><?php include 'ns/i.php';?></noscript>
<div class="row">
<div class="column">
<br><img id="genius" title="unknown" alt="unknown album" width="86%" height="auto9" src="na.jpg"/><br>
<img id="genius2" title="Daniel Doubet's Talk Show" alt="unknown album" width="86%" height="auto9" src="na.jpg" style="display:none"/><br>
<br><h4 id="dwdnp"></h4><br>
<audio controls autoplay title="" artist="" id="stream">
<source src="https://radio.dou.bet/stream" type="audio/mp3"/>
<source src="https://radio.dou.bet/stream" type="audio/mpeg"/>
</audio><br><br>
<!--<button id="controls2" class="coolbutton2">Talk Show</button><br><br>-->
<button id="lbut" class="coolbutton" style="display:inline-block">Show Lyrics</button><br><br>
<?php
/**
* index.php — main page.
* All content comes from config()/.env; markup below is station-agnostic.
*/
require_once __DIR__ . '/config.php';
<span id="spoiler" class="hiddenl"></span>
</div>
</div>
<div id="last_played">Song List</div>
<span id="spoiler2" class="hiddenl2"></span>
<span id="peep"></span>
<script id="nps" src="js/d.js"></script>
$fallbackArt = cfg('artwork.fallback');
$lyricsEnabled = cfg('lyrics.enabled') && cfg('genius.token') !== '';
$showHistory = cfg('features.show_history');
$showListeners = cfg('features.show_listeners');
?>
<!DOCTYPE html>
<html lang="<?= e(cfg('site.lang')) ?>" dir="ltr" prefix="og: http://ogp.me/ns#">
<head>
<?php require __DIR__ . '/partials/head.php'; ?>
</head>
<body>
<noscript><?php require __DIR__ . '/partials/noscript.php'; ?></noscript>
<main class="row">
<div class="column">
<img id="art" class="album-art" src="<?= e($fallbackArt) ?>"
alt="Album art" title="<?= e(cfg('site.name')) ?>"/>
<h1 class="np" id="np"><?= e(cfg('site.name')) ?></h1>
<audio id="stream" class="player" controls preload="none"<?= cfg('stream.url') !== '' ? '' : ' data-nostream' ?>>
<?php if (cfg('stream.url') !== ''): ?>
<source src="<?= e(cfg('stream.url')) ?>" type="<?= e(cfg('stream.type')) ?>"/>
<?php endif; ?>
Your browser does not support the audio element.
</audio>
<?php if ($lyricsEnabled): ?>
<button id="lyrics-toggle" class="btn-accent" hidden>Show lyrics</button>
<div id="lyrics" class="lyrics" hidden></div>
<?php endif; ?>
</div>
</main>
<?php if ($showHistory): ?>
<button id="history-toggle" class="history-toggle">Song list</button>
<aside id="history" class="history">
<table class="history-table">
<thead><tr><th>Time</th><th>Artist</th><th>Title</th></tr></thead>
<tbody id="history-body"></tbody>
</table>
</aside>
<?php endif; ?>
<?php if ($showListeners): ?>
<div id="listeners" class="listeners" aria-label="Current listeners"></div>
<?php endif; ?>
<script>
window.RADIO_CONFIG = <?= json_encode(radio_client_config(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
</script>
<script src="assets/js/player.js" defer></script>
</body>
</html>
-179
View File
@@ -1,179 +0,0 @@
// Send artist info to Matomo
function sendTrackInfo(action, name, value){
if(action&&name&&value){
_paq.push(['trackEvent', action, name, value]);
}
}
// Show/hide lyrics
$(document).ready(function(){
$('.coolbutton').click(function() {
if ($(this).text() === 'Show Lyrics') {
$('.coolbutton').text('Hide Lyrics');
$('#spoiler').toggle(750);
} else {
$('.coolbutton').text('Show Lyrics');
$('#spoiler').toggle(500);
}
});
});
// Get artwork
function getArtwork(artist, title) {
$.ajax({
type: 'POST',
data: ({ artist: artist, title: title}),
url: 'title.php'
}).then(function (data) {
var itReturned = data.split("|,=");
var art_img = itReturned[0];
var song_title = itReturned[2];
var song_album = itReturned[5];
$('#genius').attr('src', art_img);
$('#genius').attr('title', 'From the album "' + song_album + '", ' + song_title);
$('#genius').attr('alt', 'From the album "' + song_album + '", ' + song_title);
$('#metaimage1').attr('content', art_img);
$('#metaimage2').attr('content', art_img);
$('#metaimage3').attr('href', art_img);
$('#metaimage4').attr('href', art_img);
$('#metaimage5').attr('href', art_img);
}).fail(function (e) {
console.log(e);
});
}
// Get lyrics
function getLyrics(artist, title, route) {
$.ajax({
type: 'POST',
data: ({ artist: artist, title: title, route: route}),
url: 'lyrics.php'
}).then(function (data) {
var lyric_text = decodeURI(decodeURI(data));
if(lyric_text==="unknown" || lyric_text==="N/A" || lyric_text==="This song is an instrumental"){
$('.coolbutton').hide(1000);
} else {
$('.coolbutton').show(1000);
}
$('#spoiler').html(lyric_text + "<br><br><br><br>");
}).fail(function (e) {
console.log(e);
});
}
// Return Artist - Title or Title based on this function
function oddOrEven(x) {
return ( x & 1 ) ? "odd" : "even";
}
// Add leading zero for timestamp in Song List table
function addZero(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
// Populate Song List table
var superT = "dwd";
function updateSongListTbl(artistr2, titler2) {
var d = new Date();
var h2 = d.toLocaleString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
var tablenp = '<table class="songlist"><tr style="border:2px solid #000"><th style="border:2px solid #000">Time</th><th style="border:2px solid #000">Artist</th><th style="border:2px solid #000">Title</th></tr>';
var info = artistr2+' - '+titler2;
if (superT === "dwd"){
superT = info;
$('#spoiler2').html(tablenp+'<tr><td style="border:1px solid #000;">'+h2+'</td><td style="border:1px solid #000;">'+artistr2+'</td><td style="border:1px solid #000;">'+titler2+"</td></tr></table>");
sendTrackInfo('Song', artistr2, titler2);
$(getArtwork(artistr2, titler2));
$(getLyrics(artistr2, titler2, 'yes'));
} else if(info !== superT){
superT = info;
var prevSongs = $('#spoiler2').html();
prevSongs = prevSongs.replace("</table>","");
$('#spoiler2').html(prevSongs+'<tr><td style="border:1px solid #000;">'+h2+'</td><td style="border:1px solid #000;">'+artistr2+'</td><td style="border:1px solid #000;">'+titler2+"</td></tr></table>");
sendTrackInfo('Song', artistr2, titler2);
$(getArtwork(artistr2, titler2));
$(getLyrics(artistr2, titler2, 'yes'));
}
}
// Update artist and title every 10 seconds
var i = 1;
function updateTitle() {
$.ajax({
type: 'GET',
url: 'https://radio.dou.bet/status-json.xsl',
jsonpCallback: 'icestats',
dataType: 'json'
}).then(function (data) {
var json_title = data.icestats.source.title;
var json_table = json_title.split(" <-|-> ", 2);
var song_artist = json_table[0];
var song_title = json_table[1];
document.getElementById("track-title").innerHTML = song_artist + ' - ' + song_title;
document.getElementById("dwdnp").innerHTML = song_artist + ' - ' + song_title;
updateSongListTbl(song_artist, song_title);
$('#stream').attr('artist',song_artist);
if(oddOrEven(i)==="odd"){
$('#stream').attr('title', song_artist + ' - ' + song_title);
} else {
$('#stream').attr('title', song_title);
}
}).fail(function (e) {
console.log(e);
}).always(function () {
i++;
setTimeout(updateTitle, 10000);
});
}
$(updateTitle);
// Update peers every 60 seconds
function queryPeers(){
$.ajax({
type: 'GET',
url: 'https://radio.dou.bet/peers.xsl',
jsonpCallback: 'icestats',
dataType: 'json',
success: function(data){
var peak = data.icestats.source.listener_peak;
var cur = data.icestats.source.listeners;
var peers = cur + ' / ' + peak;
var link = '<a style="color:white" target="_blank" href="https://dir.xiph.org/search?q=doubzstep">' + peers + '</a>&nbsp;';
document.getElementById("peep").innerHTML = link;
setTimeout(function(){queryPeers();}, 60000);
},
error: function(e){
console.log(e);
}
});
};
$(queryPeers);
// Mobile device test
var isMobile = false;
if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) {
isMobile = true;
}
// Gently show the Song List table
$(document).ready(function(){
$('#last_played').click(function() {
if ($(this).text() === 'Song List') {
$('#last_played').text('Hide');
if(isMobile){
$('#last_played').animate({right: '392px',top: '8px',width: '60px'}, 1000);
$('.hiddenl2').toggle().animate({width: '400px'}, 1000);
} else {
$('#last_played').animate({right: '508px',top: '8px',width: '60px'}, 1000);
$('.hiddenl2').toggle().animate({width: '518px',overflow: 'scroll'}, 1000);
}
} else {
$('#last_played').text('Song List');
$('#last_played').animate({right: '-22px',top: '22px',width: '90px'}, 1000)
$('.hiddenl2').toggle().animate({width: '0',overflow:'hidden'}, 1000);
}
});
});
-203
View File
@@ -1,203 +0,0 @@
<?php
// Load environmental variables
if (file_exists('.env')){
$env = parse_ini_file('.env');
$genius = $env['GENIUS_API_BASE64'];
$debug = $env['DEBUG'] ? true : $env['DEBUG'];
$show_err = $env['SHOW_ERRORS'] ? true : $env['SHOW_ERRORS'];
} else {
die('You forgot to create your .env file!');
}
// Show errors
if ($show_err){
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
}
// Set header info
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/event-stream');
header('Cache-Control: private, max-age=90');
header('Cache-Control: no-cache');
// Genius URLs
$geniusAPI = "https://api.genius.com/search?q=";
$geniusID = "https://api.genius.com/songs/";
$geniusurl = "https://genius.com/";
$bearer = base64_decode($genius);
// Default variables
$d = '<br/>';
$array_lyrics = [];
$lyrics = '';
$scrapeme = '';
// Exception arrays
$artist_exc = ['Steve Reich', 'BAW'];
// Get POST and/or GET variables for artist and song title
$vartist = (empty($_POST['artist'])) ? (empty($_GET['artist'])) ? 'n' : $_GET['artist'] : $_POST['artist'];
$vtitle = (empty($_POST['title'])) ? (empty($_GET['title'])) ? 'n' : $_GET['title'] : $_POST['title'];
$vroute = (empty($_POST['route'])) ? (empty($_GET['route'])) ? 'n' : $_GET['route'] : $_POST['route'];
if ($vartist === 'n' && $vtitle === 'n'){
echo "offline";
exit(0);
}
if (in_array($vartist, $artist_exc)){
echo "unknown";
exit(0);
}
$json = rawurlencode($vartist . ' - ' . $vtitle);
// Curl function
function curlThis($url, $auth, $header, $returnt, $timeoutc, $timeout, $bearer){
$exe = curl_init();
curl_setopt($exe, CURLOPT_URL, "$url");
if ($auth === 1){
curl_setopt($exe, CURLOPT_HTTPHEADER, ["Authorization: Bearer $bearer"]);
}
curl_setopt($exe, CURLOPT_HEADER, $header);
curl_setopt($exe, CURLOPT_RETURNTRANSFER, $returnt);
curl_setopt($exe, CURLOPT_CONNECTTIMEOUT, $timeoutc);
curl_setopt($exe, CURLOPT_TIMEOUT, $timeout);
$raw = curl_exec($exe);
curl_close($exe);
return $raw;
}
// Clean up raw page scrapping function
$cleanArray = array(
array("<head>", "</head>"),
//array("<noscript>", "</noscript>"),
//array("<script ", "</script>"),
//array("<script>", "</script>"),
//array("<footer ", "</footer>"),
array("<svg", "</svg>"),
array("<button ", "</button>")
);
function cleanMe($lyrics, $start, $end){
if (strpos($lyrics, $start) !== false && strpos($lyrics, $end) !== false){
$start_len = strlen($start);
$start_pos = strpos($lyrics, $start);
$end_pos = strpos($lyrics, $end);
$remove_lyrics = substr($lyrics, $start_pos, $end_pos + $start_len + 1 - $start_pos);
return str_replace($remove_lyrics, '', $lyrics);
} else {
return $lyrics;
}
}
// Query Genius API using artist and song title to get URL of lyrics by querying first result
$url = $geniusAPI . $json;
$json_genius = json_decode(curlThis($url, 1, 0, 1, 5, 4, $bearer));
$result = $json_genius->response->hits[0]->result;
$scrapeme = $result->url;
$returnURL = '<a href="' . $scrapeme . '" target="_blank">Link</a><br><br>';
//echo $url . "\n\n";
//print_r($json_genius) . "\n\n";
// Scrape lyrics from Genius
if ($vroute !== 'n'){
$raw3 = curlThis($scrapeme, 0, 0, 1, 5, 4, $bearer);
$divsearch = '<div data-lyrics-container="true" class="Lyrics__Cont';
$divinstum = '<div class="LyricsPlaceholder-';
// Clean raw data
foreach ($cleanArray as $cleanThis){
$clean_start = $cleanThis[0];
$clean_end = $cleanThis[1];
while (strpos($raw3, $clean_start) !== false){
$raw3 = cleanMe($raw3, $clean_start, $clean_end);
}
}
$lyricsraw = explode($divsearch, $raw3);
$lyric_para = count($lyricsraw);
// Find lyric containers scraped from webpage
if ($lyric_para === 1){
// If there is only one result, the song probably doesn't have lyrics and needs special attention
$lyricsraw = explode($divinstum, $raw3);
foreach (explode($d, $lyricsraw[3]) as $c1){
if (substr($c1, 0, 2) !== "__" && substr($c1, 0, 2) !== "</"){
if (strpos($c1, '</div>') === false){
array_push($array_lyrics, $c1);
} else {
array_push($array_lyrics, substr($c1, 0, strpos($c1, '</div')));
}
}
}
} else {
// For everything else with a count > 1, it follows this style of parsing
if ($debug){print_r($lyricsraw) . "\n\n\n\n";}
// The first result [0] never has lyrics in it so let's start with the second result
$lr = 1;
while ($lr <= $lyric_para - 1){
if ($lr === 1){
// If this is the first time, there is preceeding garbage everywhere
// so we must loop through each results looking for our special delimeter $d
$last_exp = explode('</div>', $lyricsraw[$lr]);
foreach ($last_exp as $c1){
if (strpos($c1, $d) !== false){
if (strpos($c1, '">') === false){
array_push($array_lyrics, $c1);
} else {
array_push($array_lyrics, substr($c1, strpos($c1, '">') + 2, strlen($c1) - strpos($c1, '">') + 2));
}
}
}
} elseif ($lr === $lyric_para - 1){
// If this is the last result, we look for the first occurance of '</div>' and
// truncate the rest as it is garbage div stuff
foreach (explode($d, $lyricsraw[$lr]) as $c1){
if (substr($c1, 0, 2) !== "__" && substr($c1, 0, 2) !== "</"){
if (strpos($c1, '</div>') === false){
array_push($array_lyrics, $c1);
} else {
array_push($array_lyrics, substr($c1, 0, strpos($c1, '</div')));
//$lyrics = $lyrics . substr($c1, 0, strpos($c1, '</div')) . $d;
}
}
}
} else {
foreach (explode($d, $lyricsraw[$lr]) as $c1){
//if (substr($c1, 0, 2) !== "__" && substr($c1, 0, 2) !== "</"){
if (strpos($c1, '">') === false){
array_push($array_lyrics, $c1);
} else {
array_push($array_lyrics, substr($c1, strpos($c1, '">') + 2, strlen($c1) - strpos($c1, '">') + 2));
}
}
}
$lr++;
} // end while
} // end if
// Join lyric containers scraped from webpage
$lyrics = '';
foreach ($array_lyrics as $L){
if (strpos($L, '">') === false){
$lyrics = $lyrics . $L . $d;
} else {
$lyrics = $lyrics . substr($L, strpos($L, '">') + 2, strlen($L) - strpos($L, '">') + 2) . $d;
}
}
// Replace all href's with full link to Genius
$lyrics = str_replace('href="/','target="_blank" href="' . $geniusurl, $lyrics);
} else {
$lyrics = 'N/A';
}
// If routine couldn't find anything, return "unknown"
if (!$lyrics || $lyrics === $d){$lyrics = "unknown";}
// Return data separated with delimeter "|,="
if ($debug){
echo "$returnURL\n\n$d\n\n$lyrics";
} else {
echo "$lyrics";
}
-25
View File
@@ -1,25 +0,0 @@
<title id="track-title">Dou.Bet Radio</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="description" content="Listen to Doubet Radio 24/7 with all the pop, rock, and electronic music songs you can handle!"/>
<meta name="keywords" content="radio, internet radio, dou.bet, coding, blog, hobby, hobbies"/>
<meta name="viewport" content="width=device-width, initial-scale=0.75, maximum-scale=1.0"/>
<meta name="msapplication-TileColor" content="#2d89ef"/>
<meta name="theme-color" content="#ffffff"/>
<meta content="https://radio.dou.bet/seo/android-chrome-512x512.png">
<meta property="og:title" content="Dou.Bet Radio"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://radio.dou.bet/"/>
<meta property="og:image" content="https://radio.dou.bet/seo/ddfb.png"/>
<meta property="og:description" content="Listen to Doubet Radio 24/7 with all the pop, rock, and electronic music songs you can handle!"/>
<meta property="og:site_name" content="Dou.Bet"/>
<meta property="og:audio" content="http://radio.dou.bet/stream" />
<meta property="og:audio:secure_url" content="https://radio.dou.bet/stream" />
<meta property="og:audio:type" content="audio/mpeg" />
<link rel="apple-touch-icon" sizes="180x180" href="../seo/apple-touch-icon.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="..seo/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="../seo/favicon-16x16.png"/>
<link rel="manifest" href="../seo/site.webmanifest"/>
<link rel="mask-icon" href="../seo/safari-pinned-tab.svg" color="#666666"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"/>
<link rel="stylesheet" type="text/css" href="../css/radio.css"/>
-7
View File
@@ -1,7 +0,0 @@
<div class="row">
<div class="column">
<h2>Live Radio</h2>
<h4>To get artist, title, album art, and lyrics, please enable JavaScript.</h4>
<audio id="stream" controls><source src="https://radio.dou.bet/stream" type="audio/mpeg"/></audio>
</div>
</div>
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* partials/head.php
* -----------------
* Everything inside <head>, driven entirely by config(). No station-specific
* strings live here — change your .env, not this file.
*/
if (!defined('RADIO_CONFIG_LOADED')) { http_response_code(500); exit('config not loaded'); }
$siteName = cfg('site.name');
$siteUrl = cfg('site.url');
$ogImage = cfg('seo.og_image');
$iconsDir = cfg('seo.icons_dir');
?>
<meta charset="utf-8"/>
<title id="track-title"><?= e($siteName) ?></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="<?= e(cfg('site.description')) ?>"/>
<meta name="keywords" content="<?= e(cfg('site.keywords')) ?>"/>
<?php if (cfg('site.author') !== ''): ?>
<meta name="author" content="<?= e(cfg('site.author')) ?>"/>
<?php endif; ?>
<meta name="theme-color" content="<?= e(cfg('seo.theme_color')) ?>"/>
<meta name="msapplication-TileColor" content="<?= e(cfg('seo.tile_color')) ?>"/>
<!-- Open Graph -->
<meta property="og:title" content="<?= e($siteName) ?>"/>
<meta property="og:type" content="website"/>
<meta property="og:site_name" content="<?= e(cfg('site.short_name')) ?>"/>
<meta property="og:description" content="<?= e(cfg('site.description')) ?>"/>
<meta property="og:locale" content="<?= e(cfg('site.locale')) ?>"/>
<?php if ($siteUrl !== '/'): ?>
<meta property="og:url" content="<?= e($siteUrl) ?>"/>
<?php endif; ?>
<?php if ($ogImage !== ''): ?>
<meta property="og:image" id="metaimage1" content="<?= e($ogImage) ?>"/>
<?php endif; ?>
<?php if (cfg('stream.url') !== ''): ?>
<meta property="og:audio" content="<?= e(cfg('stream.url')) ?>"/>
<meta property="og:audio:type" content="<?= e(cfg('stream.type')) ?>"/>
<?php endif; ?>
<?php if (cfg('seo.show_icons')): ?>
<link rel="apple-touch-icon" sizes="180x180" href="<?= e($iconsDir) ?>/apple-touch-icon.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="<?= e($iconsDir) ?>/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="<?= e($iconsDir) ?>/favicon-16x16.png"/>
<link rel="manifest" href="<?= e($iconsDir) ?>/site.webmanifest"/>
<?php endif; ?>
<!-- Theme variables (from .env) -->
<style>
:root{
--bg: <?= e(cfg('theme.bg')) ?>;
--bg-mobile: <?= e(cfg('theme.bg_mobile')) ?>;
--surface: <?= e(cfg('theme.surface')) ?>;
--text: <?= e(cfg('theme.text')) ?>;
--muted: <?= e(cfg('theme.muted')) ?>;
--accent: <?= e(cfg('theme.accent')) ?>;
--accent-hover: <?= e(cfg('theme.accent_hover')) ?>;
--accent-text: <?= e(cfg('theme.accent_text')) ?>;
--radius: <?= e(cfg('theme.radius')) ?>;
--font: <?= cfg('theme.font') /* font stack, not user text */ ?>;
}
</style>
<link rel="stylesheet" href="assets/css/radio.css"/>
<?php if (cfg('analytics.matomo_url') !== '' && cfg('analytics.matomo_site_id') !== ''):
$mUrl = rtrim(cfg('analytics.matomo_url'), '/') . '/'; ?>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = "<?= e($mUrl) ?>";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '<?= e(cfg('analytics.matomo_site_id')) ?>']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true; g.src = u + 'matomo.js'; s.parentNode.insertBefore(g, s);
})();
</script>
<?php endif; ?>
+21
View File
@@ -0,0 +1,21 @@
<?php
/**
* partials/noscript.php
* ---------------------
* Shown when JavaScript is disabled: station name and a bare audio player so
* people can still listen. Artwork, live titles, and lyrics need JS.
*/
if (!defined('RADIO_CONFIG_LOADED')) { exit; }
?>
<div class="row">
<div class="column">
<h1><?= e(cfg('site.name')) ?></h1>
<p><?= e(cfg('site.tagline')) ?></p>
<p>Enable JavaScript for track info, album art, and lyrics.</p>
<?php if (cfg('stream.url') !== ''): ?>
<audio controls preload="none">
<source src="<?= e(cfg('stream.url')) ?>" type="<?= e(cfg('stream.type')) ?>"/>
</audio>
<?php endif; ?>
</div>
</div>
-191
View File
@@ -1,191 +0,0 @@
<?php
// Load environmental variables
if (file_exists('.env')){
$env = parse_ini_file('.env');
$genius = $env['GENIUS_API_BASE64'];
$debug = $env['DEBUG'] ? true : $env['DEBUG'];
$show_err = $env['SHOW_ERRORS'] ? true : $env['SHOW_ERRORS'];
} else {
die('You forgot to create your .env file!');
}
// Testing file always shows errors
$debug = true;
$show_err = true;
// Show errors
if ($show_err){
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
}
// Set header info
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/event-stream');
header('Cache-Control: private, max-age=90');
header('Cache-Control: no-cache');
// Genius URLs
$geniusAPI = "https://api.genius.com/search?q=";
$geniusID = "https://api.genius.com/songs/";
$geniusurl = "https://genius.com/";
$bearer = base64_decode($genius);
// Default variables
$lyrics = '';
$scrapeme = '';
// Get POST and/or GET variables for artist and song title
$vartist = (empty($_POST['artist'])) ? (empty($_GET['artist'])) ? 'n' : $_GET['artist'] : $_POST['artist'];
$vtitle = (empty($_POST['title'])) ? (empty($_GET['title'])) ? 'n' : $_GET['title'] : $_POST['title'];
$vroute = (empty($_POST['route'])) ? (empty($_GET['route'])) ? 'n' : $_GET['route'] : $_POST['route'];
if ($vartist === 'n' && $vtitle === 'n'){
echo "offline";
exit(0);
}
$json = rawurlencode($vartist . ' - ' . $vtitle);
// Curl function
function curlThis($url, $auth, $header, $returnt, $timeoutc, $timeout, $bearer){
$exe = curl_init();
curl_setopt($exe, CURLOPT_URL, "$url");
if ($auth === 1){
curl_setopt($exe, CURLOPT_HTTPHEADER, ["Authorization: Bearer $bearer"]);
}
curl_setopt($exe, CURLOPT_HEADER, $header);
curl_setopt($exe, CURLOPT_RETURNTRANSFER, $returnt);
curl_setopt($exe, CURLOPT_CONNECTTIMEOUT, $timeoutc);
curl_setopt($exe, CURLOPT_TIMEOUT, $timeout);
$raw = curl_exec($exe);
curl_close($exe);
return $raw;
}
// Clean up raw page scrapping function
$d = '<br/>';
$cleanArray = array(
array("<head>", "</head>"),
array("<noscript>", "</noscript>"),
array("<script ", "</script>"),
array("<script>", "</script>"),
array("<footer ", "</footer>"),
array("<svg", "</svg>"),
array("<button ", "</button>")
);
function cleanMe($lyrics, $start, $end){
if (strpos($lyrics, $start) !== false && strpos($lyrics, $end) !== false){
$start_len = strlen($start);
$start_pos = strpos($lyrics, $start);
$end_pos = strpos($lyrics, $end);
$remove_lyrics = substr($lyrics, $start_pos, $end_pos + $start_len + 1 - $start_pos);
return str_replace($remove_lyrics, '', $lyrics);
} else {
return $lyrics;
}
}
// Query Genius API using artist and song title to get URL of lyrics by querying first result
$url = $geniusAPI . $json;
$json_genius = json_decode(curlThis($url, 1, 0, 1, 5, 4, $bearer));
$result = $json_genius->response->hits[0]->result;
$scrapeme = $result->url;
$returnURL = '<a href="' . $scrapeme . '" target="_blank">Link</a><br><br>';
//echo $url . "\n\n";
//print_r($json_genius) . "\n\n";
// Scrape lyrics from Genius
if ($vroute !== 'n'){
$raw3 = curlThis($scrapeme, 0, 0, 1, 5, 4, $bearer);
$divsearch = '<div data-lyrics-container="true" class="Lyrics__Cont';
$divinstum = '<div class="LyricsPlaceholder-';
$divend = '<div class="LyricsFooter-';
// Clean raw data
foreach ($cleanArray as $cleanThis){
$clean_start = $cleanThis[0];
$clean_end = $cleanThis[1];
while (strpos($raw3, $clean_start) !== false){
$raw3 = cleanMe($raw3, $clean_start, $clean_end);
}
}
$lyricsraw = explode($divsearch, $raw3);
$lyric_para = count($lyricsraw);
if ($debug){echo "$lyric_para\n\n\n\n";}
if ($debug){print_r($lyricsraw) . "\n\n\n\n";}
// Find lyric containers scraped from webpage
$array_lyrics = [];
if ($lyric_para === 1){
// If there is only one result, the song probably doesn't have lyrics and needs special attention
$lyricsraw = explode($divinstum, $raw3);
foreach (explode($d, $lyricsraw[3]) as $c1){
if (substr($c1, 0, 2) !== "__" && substr($c1, 0, 2) !== "</"){
if (strpos($c1, '</div>') === false){
$lyrics = $lyrics . $c1 . $d;
} else {
$lyrics = $lyrics . substr($c1, 0, strpos($c1, '</div')) . $d;
}
}
}
} else {
// For everything else with a count > 1, it follows this style of parsing
if ($debug){print_r($lyricsraw) . "\n\n\n\n";}
//$array_filter = ['aine', '</a>', '<div', '</as'];
$lr = 1;
while ($lr <= $lyric_para - 1){
if ($lr === 1){
$last_exp = explode('</div>', $lyricsraw[$lr]);
foreach ($last_exp as $c1){
if (strpos($c1, $d) !== false){
array_push($array_lyrics, $c1);
}
}
} elseif ($lr === $lyric_para - 1){
foreach (explode($d, $lyricsraw[$lr]) as $c1){
if (substr($c1, 0, 2) !== "__" && substr($c1, 0, 2) !== "</"){
if (strpos($c1, '</div>') === false){
array_push($array_lyrics, $c1);
} else {
array_push($array_lyrics, substr($c1, 0, strpos($c1, '</div')));
//$lyrics = $lyrics . substr($c1, 0, strpos($c1, '</div')) . $d;
}
}
}
} else {
foreach (explode($d, $lyricsraw[$lr]) as $c1){
if (substr($c1, 0, 2) !== "__" && substr($c1, 0, 2) !== "</"){
array_push($array_lyrics, $c1);
}
}
}
$lr++;
} // end while
} // end if
// Join lyric containers scraped from webpage
//if ($debug){print_r($array_lyrics) . "\n\n\n\n";}
$lyrics = '';
foreach ($array_lyrics as $l){
$lyrics = $lyrics . $l . $d;
}
// Replace all href's with full link to Genius
$lyrics = str_replace('href="/','target="_blank" href="' . $geniusurl, $lyrics);
} else {
$lyrics = 'N/A';
}
// If routine couldn't find anything, return "unknown"
if (!$lyrics || $lyrics === $d){$lyrics = "unknown";}
// Return data separated with delimeter "|,="
if ($debug){
echo "$returnURL\n\n$d\n\n$lyrics";
} else {
echo "$lyrics";
}
-154
View File
@@ -1,154 +0,0 @@
<?php
// Load environmental variables
if (file_exists('.env')){
$env = parse_ini_file('.env');
$genius = $env['GENIUS_API_BASE64'];
$debug = $env['DEBUG'] ? 1 : $env['DEBUG'];
$show_err = $env['SHOW_ERRORS'] ? 1 : $env['SHOW_ERRORS'];
} else {
die('You forgot to create your .env file!');
}
// Show errors
if ($show_err){
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
}
// Set header info
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/event-stream');
header('Cache-Control: private, max-age=90');
header('Cache-Control: no-cache');
// Genius URLs
$geniusAPI = "https://api.genius.com/search?q=";
$geniusID = "https://api.genius.com/songs/";
$geniusurl = "https://genius.com/";
$bearer = base64_decode($genius);
// Default variables
$random_number = rand(0, 100);
$d = '|,=';
$art = '';
$title = '';
$ftitle = '';
$lyrics = '';
$scrapeme = '';
$album = '';
// Get POST and/or GET variables for artist and song title
$vartist = (empty($_POST['artist'])) ? (empty($_GET['artist'])) ? 'n' : $_GET['artist'] : $_POST['artist'];
$vtitle = (empty($_POST['title'])) ? (empty($_GET['title'])) ? 'n' : $_GET['title'] : $_POST['title'];
$vroute = (empty($_POST['route'])) ? (empty($_GET['route'])) ? 'n' : $_GET['route'] : $_POST['route'];
if ($vartist === 'n' && $vtitle === 'n'){
echo "offline|,=offline|,=offline|,=offline|,=offline|,=offline";
exit(0);
}
// Replace artwork based on artist and song title
function customArt($artist, $title){
$customArt = array(
//array("Artist", "Song", "Art URL"),
array("BAW", "CHILDHOOD MEMORIES", "https://i1.sndcdn.com/artworks-zNyvrtxKfGAu60Yr-e7fGPA-t500x500.jpg"),
array("BAW", "happy (fracture)", "https://i1.sndcdn.com/artworks-KzJr6d6rIpSZFYM3-enWyJQ-t500x500.jpg"),
array("BAW", "RO t TEN", "https://i1.sndcdn.com/artworks-zXM1fWSPPcKdTBzy-8NpODA-t500x500.jpg"),
array("Steve Reich", "", "https://stevereich.com/wp-content/uploads/Steve-Reich-J.Herman-e1654903069903-400x340.jpeg")
);
if (array_search($artist, array_column($customArt, 0)) !== false && array_search($title, array_column($customArt, 1)) !== false) {
$ca = array_search($title, array_column($customArt, 1));
return $customArt[$ca][2];
} elseif (array_search($artist, array_column($customArt, 0)) !== false) {
$ca = array_search($artist, array_column($customArt, 0));
return $customArt[$ca][2];
} else {
return "no";
}
}
$custom_artwork = customArt($vartist, $vtitle);
if ($custom_artwork !== 'no'){
echo $custom_artwork . $d . $vtitle . $d . $vtitle . $d . $lyrics . $d . $scrapeme . $d . $album;
exit(0);
}
$json = rawurlencode($vartist . ' - ' . $vtitle);
// Curl function
function curlThis($url, $auth, $header, $returnt, $timeoutc, $timeout, $bearer){
$exe = curl_init();
curl_setopt($exe, CURLOPT_URL, "$url");
if ($auth === 1){
curl_setopt($exe, CURLOPT_HTTPHEADER, ["Authorization: Bearer $bearer"]);
}
curl_setopt($exe, CURLOPT_HEADER, $header);
curl_setopt($exe, CURLOPT_RETURNTRANSFER, $returnt);
curl_setopt($exe, CURLOPT_CONNECTTIMEOUT, $timeoutc);
curl_setopt($exe, CURLOPT_TIMEOUT, $timeout);
$raw = curl_exec($exe);
curl_close($exe);
return $raw;
}
// Query Genius API using artist and song title to get URL of lyrics by querying first result
$url = $geniusAPI . $json;
$json_genius = json_decode(curlThis($url, 1, 0, 1, 5, 4, $bearer));
$result = $json_genius->response->hits[0]->result;
$songid = $result->id;
$scrapeme = $result->url;
$artart = $result->primary_artist->image_url;
// Scrape lyrics from Genius
$lyrics = 'N/A';
// Get album information from Genius API using songid
$url = $geniusID . $songid;
$get2 = json_decode(curlThis($url, 1, 0, 1, 5, 4, $bearer));
$songInfo = $get2->response->song;
$title = $songInfo->title;
$ftitle = $songInfo->full_title;
$album = $songInfo->album->name;
$art = $songInfo->header_image_url;
// Double check if we have data and try something else if no data yet
if (!$album){$album = $ftitle;}
if (!$title){$title = $result->title;}
if (!$ftitle){$ftitle = $result->full_title;}
if (!$art){$art = $result->header_image_url;}
// Replace default Genius images with cool GIFs
function badArt($arturl){
$badart = ['https://radio.dou.bet/na.jpg','https://images.genius.com/9d53cf21e85632e8a774b470fc1af6d9.960x960x1.jpg','https://images.genius.com/5bb0d83dd9b4f5f97b7cea2e16091892.1000x1000x1.png','https://images.genius.com/4defb2d0b45e544711e5dbabe266a370.1000x1000x1.png',
'https://images.genius.com/e3f28a8c15ae3b11ac9cc7aaef0396c1.512x512x1.jpg','https://images.genius.com/2aa2941e1d8ed0034c2ddc9dd5012af9.1000x1000x1.png','https://assets.genius.com/images/default_cover_image.png',
'https://assets.genius.com/images/default_cover_image.jpg','https://images.genius.com/cc43e9f65bda58676823af326760364a.999x999x1.png'];
$childish = ['https://images.genius.com/2114312d382ad914c2c7b6146fbfb6c2.300x300x1.jpg','https://images.genius.com/90d733d87dcdbe5bb5f89062e6fb381f.500x500x1.jpg','https://images.genius.com/90d733d87dcdbe5bb5f89062e6fb381f.500x500x1.jpg'];
if (!$arturl || in_array($arturl, $badart)){
if ($random_number <= 40){
$art = "https://78.media.tumblr.com/a38fa7ca0f62769471df65b4ce8d177d/tumblr_pogjorskHk1w3y4ilo2_540.gif";
} elseif ($random_number <= 70){
$art = "https://i.imgur.com/PWw7VmH.gif";
} else {
$art = "https://i.pinimg.com/originals/d7/18/9b/d7189b9a574f7c279473847357a91f69.gif";
}
} else {
$art = $arturl;
}
if (in_array($arturl, $childish)){
$art = "https://miro.medium.com/v2/resize:fit:632/1*eswZSNBeUgzlczhy7qA9Nw.gif";
}
return $art;
}
$art = badArt($art);
// If routine couldn't find anything, return "unknown"
if (!$title){$title = $vtitle;}
if (!$ftitle){$ftitle = "unknown";}
if (!$lyrics){$lyrics = "unknown";}
if (!$scrapeme){$scrapeme = "unknown";}
if(!$album){$album = "unknown";}
// Return data separated with delimeter "|,="
echo "$art|,=$title|,=$ftitle|,=$lyrics|,=$scrapeme|,=$album";