Refector code
This commit is contained in:
+147
@@ -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
|
||||
@@ -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
|
||||
|
||||
| [](https://pix.dou.bet/image/Arc-AtfdsjDppb.gZNv) |
|
||||
| :---: |
|
||||
| [](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).
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
@@ -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
@@ -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]);
|
||||
@@ -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);
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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 |
@@ -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
@@ -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
@@ -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 */
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
[env]
|
||||
GENIUS_API_BASE64=
|
||||
DEBUG=true
|
||||
SHOW_ERRORS=true
|
||||
@@ -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
@@ -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"/>
|
||||
@@ -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>
|
||||
|
||||
@@ -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> ';
|
||||
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
@@ -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
@@ -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"/>
|
||||
@@ -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>
|
||||
@@ -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; ?>
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user