diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5a0c5c9..513dd47 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -50,7 +50,7 @@ jobs: # Multi platform image build and push - name: Build and push the latest Docker image - run: docker buildx build --platform "$PLATFORMS" . --file "Dockerfile" --tag "$IMAGE_OWNER/$IMAGE_NAME:latest" --push + run: docker buildx build --platform "$PLATFORMS" . --file "./utils/Dockerfile" --tag "$IMAGE_OWNER/$IMAGE_NAME:latest" --push if: ${{ (github.event_name == 'push') || (steps.check.outputs.needs-updating == 'true') }} - name: Push more tags @@ -61,6 +61,6 @@ jobs: echo "Skipping $TAG"; else echo "Tagging image as $TAG and pushing"; - docker buildx build --platform "$PLATFORMS" . --file "Dockerfile" --tag "$IMAGE_OWNER/$IMAGE_NAME:$TAG" --push + docker buildx build --platform "$PLATFORMS" . --file "./utils/Dockerfile" --tag "$IMAGE_OWNER/$IMAGE_NAME:$TAG" --push fi; done; diff --git a/.gitignore b/.gitignore index aebd021..a702879 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -/php/data/log.txt +/php/data/*.log /php/data/cache +/php/getr.php + /data/ /redis/ -/php/getr.php -.DS_Store +.DS_Store \ No newline at end of file diff --git a/.phan/config.php b/.phan/config.php index 188a343..0aedfdf 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -13,9 +13,9 @@ return [ 'target_php_version' => '8.0', 'file_list' => [ - 'cron.php', - 'startup.php', - 'getr.php' + './utils/cron.php', + './utils/startup.php', + './utils/getr.php' ], 'directory_list' => [ 'php' diff --git a/Readme.md b/Readme.md index 0d7bc7d..21808c8 100644 --- a/Readme.md +++ b/Readme.md @@ -18,7 +18,7 @@ This redirect is possible by manipulating the DNS queries. → [Have a look at **screenshots**](./screenshots/Readme.md) ## Usage -- First [set up](./Setup.md) the Docker Container of this *Radio-API* and change the DNS resolver of the radio (e.g., as described there). +- First [set up](./Setup.md) *Radio-API* and change the DNS resolver of the radio (e.g., as described there). - Afterwards start the radio and open "Internet Radio". - The *Radio-API* should provide a list of: - **Podcast** diff --git a/Setup.md b/Setup.md index 3561bf1..bf16ed6 100644 --- a/Setup.md +++ b/Setup.md @@ -57,9 +57,10 @@ The image of [Radio DNS](https://hub.docker.com/r/kimbtechnologies/radio_dns) is - The manual setup does not rely on *Redis* (which is replaced by a file-based caching). - The only requirement a current version of PHP (code analysis shows compatibility with PHP > 8.0, code is tested with 8.2 and 8.3). - You do not need a cron job, all data is stored in `./data/` and the cache files in `./data/cache/`. + - You may change the folder for cache files to, e.g., a ramdisk. If you do so, use the script `./utils/backup-restore.php` to backup data which is only stored by the cache (using Docker this is done by the cron job). - The proxy feature is provided by PHP, but might be less stable than the NGINX proxy. - The EndURL feature uses the cURL extension of PHP (else it will error!). - - Assure, that PHP/ the webserver can write to `./data/`! + - Assure, that PHP/ the webserver can write to `./data/` (and the folders configured for logs and cache files)! - Download the lastest source of the *Radio-API* [here](https://github.com/KIMB-technologies/Radio-API/releases/latest). - Extract the zip and place the folder `php` in the web-root of our server (this is our `./`, other files are not needed). - Configure *Radio-API* in `./data/env.json` (The config values are the same as for the Docker-based mode, always use strings for the values!): @@ -70,6 +71,8 @@ The image of [Radio DNS](https://hub.docker.com/r/kimbtechnologies/radio_dns) is - `CONF_SHUFFLE_MUSIC` Randomly shuffle music in Nextcloud radio stations - `CONF_CACHE_EXPIRE` Cache duration of ips, podcasts, RadioBrowser requests, ... - `CONF_STREAM_JSON` Url to a JSON list of streams or `false` to disable (see [Own Streams](#own-streams)). + - `CONF_LOG_DIR` (optional) Change the folder where log files are written to (defaults to `./data/`). + - `CONF_CACHE_DIR` (optional) Change the folder used by the file based cache (defaults to `./data/cache/`). - Make sure, that *Radio-API* is available at port `80` for requests with the hostname `*.wifiradiofrontier.com` and `CONF_DOMAIN`. - Block HTTP access to `./data/` (and `./classes/`) for security reasons (might be omitted in a local network installation). - Rewrite requests to PHP: @@ -78,11 +81,23 @@ The image of [Radio DNS](https://hub.docker.com/r/kimbtechnologies/radio_dns) is - `./index.php` checks the get parameter `uri` for the path value, e.g., `"/setupapp/iden/asp/BrowseXML/loginXML.asp"`. If this fails, `$_SERVER['REQUEST_URI']` is checked and the part before the first `?` is taken as path value. - It is important, that the path value starts with `/` and contains the full path, but without get parameters starting at `?`. - - See the example for NGINX below. The built in webserver of PHP may be used for development with the `router.php` in the repository's root. + - See the example for NGINX below. The built in webserver of PHP may be used for development with the `./utils/router.php` in this repository. 3. Done - Start the radio and open `Internet Radio`. - You will see the entries described above at [Usage](./#usage). +### Updates +> This is for manual installs, Docker users must make sure to store the redis volume or to run the cron job. +> Then Radio-API can be restarted using a newer version of the Docker Image. + +- Run the `./utils/backup-restore.php` script to export all relevant data from the cache. + This will result in two files, which may be stored in `./data` or somewhere else. +- Create a copy of files in the folder `./data` (`./data/cache` can be deleted). +- Install the new version of Radio-API (download zip, extract `./php` folder to webroot). +- Copy the previously saved files back to `./data`. +- Run the `./utils/backup-restore.php` script to import all relevant data to the cache. + This will read the two files created during the export. + ### Rewrite with NGINX ```nginx @@ -109,8 +124,8 @@ location @nofile { ## General Information ### Troubleshooting -- A log file of (unknown) request received by the Radio-API is created at `./data/log.txt`. -- Errors with the RadioBrowser API are logged at `./data/log_radiobrowser.txt`. +- A log file of (unknown) request received by the Radio-API is created at `CONF_LOG_DIR/requests.log`. (`CONF_LOG_DIR` default to `./data`) +- Errors with the RadioBrowser API are logged at `CONF_LOG_DIR/radiobrowser.log`. - If the Radio-API is unable to parse a JSON-file in `./data/`, it will initialize a new one, while the old one is renamed to `*.error.json`. - PHP error messages are disabled by default, set `DEV=dev` in the environment to enable them. - Erase the data folder/ volume of redis and restart Radio-API. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4a52bf6..50ef43d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,13 +8,15 @@ services: radio_api: build: context: . + dockerfile: ./utils/Dockerfile container_name: radio_api_dev ports: - "8080:80" volumes: - ./php/:/php-code/ - ./data/:/php-code/data/ - - ./getr.php:/php-code/getr.php:ro # redis all values listing + - ./utils/getr.php:/php-code/getr.php:ro # redis all values listing + - ./utils/backup-restore.php:/backup-restore.php:ro # backup restore tool for cache values environment: - DEV=dev - CONF_DOMAIN=http://localhost:8080/ diff --git a/docker-compose.yml b/docker-compose.yml index 87b0efb..78a1a3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: # - CONF_REDIS_PORT=6379 # default 6379 # - CONF_REDIS_PASS= # default no auth - CONF_STREAM_JSON=false # to disable or an url where to fetch list of own streams, e.g., http://stream.example.com/list.json + # -CONF_LOG_DIR= # set a custom directory for log files (defaults to ./data/) depends_on: - redis redis: diff --git a/php/classes/Config.php b/php/classes/Config.php index ac50bb4..0baba63 100644 --- a/php/classes/Config.php +++ b/php/classes/Config.php @@ -28,11 +28,32 @@ } // load ENV values -define( 'ENV_DOMAIN', $ENV['CONF_DOMAIN'] . (substr( $ENV['CONF_DOMAIN'], -1) !== '/' ? '/' : '') ); -define( 'ENV_ALLOWED_DOMAIN', !empty($ENV['CONF_ALLOWED_DOMAIN']) ? $ENV['CONF_ALLOWED_DOMAIN'] : null ); -define( 'ENV_CACHE_EXPIRE', intval($ENV['CONF_CACHE_EXPIRE'])); -define( 'ENV_STREAM_JSON', !empty($ENV['CONF_STREAM_JSON']) && $ENV['CONF_STREAM_JSON'] != 'false' ? $ENV['CONF_STREAM_JSON'] : false ); -define( 'ENV_SHUFFLE_MUSIC', $ENV['CONF_SHUFFLE_MUSIC'] == 'true'); +define( 'ENV_DOMAIN', + $ENV['CONF_DOMAIN'] . (substr( $ENV['CONF_DOMAIN'], -1) !== '/' ? '/' : '') +); +define( 'ENV_ALLOWED_DOMAIN', + !empty($ENV['CONF_ALLOWED_DOMAIN']) ? + strval($ENV['CONF_ALLOWED_DOMAIN']) : null +); +define( 'ENV_CACHE_EXPIRE', + intval($ENV['CONF_CACHE_EXPIRE']) +); +define( 'ENV_STREAM_JSON', + !empty($ENV['CONF_STREAM_JSON']) && $ENV['CONF_STREAM_JSON'] != 'false' ? + strval($ENV['CONF_STREAM_JSON']) : false +); +define( 'ENV_SHUFFLE_MUSIC', + $ENV['CONF_SHUFFLE_MUSIC'] == 'true' +); +define( 'ENV_LOG_DIR', + !empty($ENV['CONF_LOG_DIR']) && realpath($ENV['CONF_LOG_DIR']) ? + realpath($ENV['CONF_LOG_DIR']) : __DIR__ . '/../data' +); +define( + 'ENV_CACHE_DIR', + !empty($ENV['CONF_CACHE_DIR']) && realpath(substr($ENV['CONF_LOG_DIR'], 0, strrpos($ENV['CONF_LOG_DIR'], '/', -2))) ? + $ENV['CONF_CACHE_DIR'] : __DIR__ . '/../data/cache' +); // IP on reverse proxy setup if( !empty($_SERVER['HTTP_X_REAL_IP']) ){ @@ -48,7 +69,7 @@ class Config { /** * The system's version. */ - const VERSION = 'v2.7.0'; + const VERSION = 'v2.7.1-dev'; /** * The real domain which should be used. @@ -68,7 +89,17 @@ class Config { /** * Random shuffle music station streams from nc */ - CONST SHUFFLE_MUSIC = ENV_SHUFFLE_MUSIC; + const SHUFFLE_MUSIC = ENV_SHUFFLE_MUSIC; + + /** + * The directory where the logfiles are stored. + */ + const LOG_DIR = ENV_LOG_DIR; + + /** + * The directory used by the json cache (replacement for Redis in non-Docker mode) + */ + const CACHE_DIR = ENV_CACHE_DIR; /** * Store redis cache for ALLOWED_DOMAINS diff --git a/php/classes/JSONCache.php b/php/classes/JSONCache.php index f5ecbf9..fc7f7e1 100644 --- a/php/classes/JSONCache.php +++ b/php/classes/JSONCache.php @@ -16,7 +16,7 @@ */ class JSONCache implements CacheInterface { - const BASE_DIR = __DIR__ . '/../data/cache'; + const BASE_DIR = Config::CACHE_DIR; private $file, $data, $cleanupRan = false; diff --git a/php/classes/RadioBrowser.php b/php/classes/RadioBrowser.php index 5d56ca3..89a3a55 100644 --- a/php/classes/RadioBrowser.php +++ b/php/classes/RadioBrowser.php @@ -61,7 +61,7 @@ public function __construct( Id|int $id ){ private function log(array $data) : void { file_put_contents( - __DIR__ . '/../data/log_radiobrowser.txt', + Config::LOG_DIR . '/radiobrowser.log', date('d.m.Y H:i:s') . " : " . json_encode( $data ) . PHP_EOL, FILE_APPEND ); @@ -434,7 +434,7 @@ public function handleStationPlay(Output $out, string $id) : void { /** * Dump all last stations to disk (called by cron) */ - public static function dumpToDisk() : bool { + public static function dumpToDisk(?string $exportDir = null) : bool { if( is_file( __DIR__ . '/../data/table.json' ) ){ $table = json_decode(file_get_contents( __DIR__ . '/../data/table.json' ), true); $redis = new Cache("radio-browser"); @@ -444,7 +444,10 @@ public static function dumpToDisk() : bool { $lasts[$id] = $redis->arrayGet('last_stations.'.$id); } - return file_put_contents(__DIR__ . '/../data/radiobrowser.json', json_encode( $lasts, JSON_PRETTY_PRINT)) !== false; + return file_put_contents( + (is_null($exportDir) ? __DIR__ . '/../data' : $exportDir) . '/radiobrowser.json', + json_encode( $lasts, JSON_PRETTY_PRINT) + ) !== false; } return true; } @@ -452,9 +455,10 @@ public static function dumpToDisk() : bool { /** * Load dumped last stations into Redis (done on container startup) */ - public static function loadFromDisk() : array { - if( is_file(__DIR__ . '/../data/radiobrowser.json') ){ - $lasts = json_decode(file_get_contents(__DIR__ . '/../data/radiobrowser.json'), true); + public static function loadFromDisk(?string $exportDir = null) : array { + $file = (is_null($exportDir) ? __DIR__ . '/../data' : $exportDir) . '/radiobrowser.json'; + if( is_file($file) ){ + $lasts = json_decode(file_get_contents($file), true); $redis = new Cache("radio-browser"); foreach( $lasts as $id => $last ){ diff --git a/php/classes/RedisCache.php b/php/classes/RedisCache.php index 814c88b..a47ef0d 100644 --- a/php/classes/RedisCache.php +++ b/php/classes/RedisCache.php @@ -62,7 +62,7 @@ private function generateKey( string $key ) : string { return $this->prefix . str_replace( ':', '', $key ); } - public function getAllKeysOfGroup() : array { + public function getAllKeysOfGroup(bool $trimPrefix = true) : array { $all = array(); $lenpref = strlen($this->prefix); $iterator = NULL; @@ -74,11 +74,14 @@ public function getAllKeysOfGroup() : array { })); } } while ($iterator > 0); + if($trimPrefix){ + $all = array_map(fn($k) => substr($k, $lenpref), $all); + } return $all; } public function removeGroup() : bool { - $dels = $this->getAllKeysOfGroup(); + $dels = $this->getAllKeysOfGroup(false); return $this->redis->unlink($dels) == count($dels); } @@ -154,7 +157,7 @@ public function output(): void { echo 'Key' . "\t\t : " . 'Value' . PHP_EOL; echo '---------------------------------' . PHP_EOL; $lenpref = strlen($this->prefix); - foreach( $this->getAllKeysOfGroup() as $fullkey ){ + foreach( $this->getAllKeysOfGroup(false) as $fullkey ){ $key = substr($fullkey, $lenpref); if( $this->redis->type($fullkey) !== Redis::REDIS_HASH ){ $val = $this->get($key); diff --git a/php/classes/Router.php b/php/classes/Router.php index afd9185..617b9df 100644 --- a/php/classes/Router.php +++ b/php/classes/Router.php @@ -98,7 +98,11 @@ public function handleGet(string $uri) : void { preg_match('/^\/setupapp\/[A-Za-z0-9\-\_]+\/asp\/BrowseXML\/loginXML.asp/i', $uri) === 0 && (!isset( $_GET['go'] ) || $_GET['go'] != "initial") ){ - file_put_contents( __DIR__ . '/../data/log.txt', date('d.m.Y H:i:s') . " : " . json_encode( $_GET ) . PHP_EOL, FILE_APPEND ); + file_put_contents( + Config::LOG_DIR . '/requests.log', + date('d.m.Y H:i:s') . " : " . json_encode( $_GET ) . PHP_EOL, + FILE_APPEND + ); } } } diff --git a/php/classes/Template.php b/php/classes/Template.php index 839ce58..92a5b62 100644 --- a/php/classes/Template.php +++ b/php/classes/Template.php @@ -213,8 +213,9 @@ public function getOutputString() : string { $htmldata = $a[0] . $middle . $b[1]; } - $this->placeholder['%%SERVERURL%%'] = 'http'. ( empty($_SERVER['HTTPS']) ? '' : 's' ) .':'. substr(Config::DOMAIN, strpos(Config::DOMAIN, '//')); - + $this->placeholder['%%SERVERURL%%'] = 'http'. ( empty($_SERVER['HTTPS']) ? '' : 's' ) .':'. + substr(Config::DOMAIN, strpos(Config::DOMAIN, '//')); + $this->placeholder['%%VERSION%%'] = Config::VERSION; if( $this->inner !== null ){ $this->placeholder['%%INNERCONTAINER%%'] = $this->inner->getOutputString(); diff --git a/php/classes/UnRead.php b/php/classes/UnRead.php index d36431c..9d1d1df 100644 --- a/php/classes/UnRead.php +++ b/php/classes/UnRead.php @@ -84,7 +84,7 @@ public function __destruct(){ /** * Dump all known podcast episodes to disk (called by cron) */ - public static function dumpToDisk() : bool { + public static function dumpToDisk(?string $exportDir = null) : bool { if( is_file( __DIR__ . '/../data/table.json' ) ){ $table = json_decode(file_get_contents( __DIR__ . '/../data/table.json' ), true); @@ -93,13 +93,14 @@ public static function dumpToDisk() : bool { $redis = new Cache('unread_podcasts.' . $id ); $reads[$id] = array(); foreach($redis->getAllKeysOfGroup() as $key ){ - if( preg_match('/^.*:([^0-9s].*)$/', $key, $matches) === 1){ - $reads[$id][] = $matches[1]; - } + $reads[$id][] = $key; } } - return file_put_contents(__DIR__ . '/../data/unread.json', json_encode( $reads, JSON_PRETTY_PRINT)) !== false; + return file_put_contents( + (is_null($exportDir) ? __DIR__ . '/../data' : $exportDir) . '/unread.json', + json_encode( $reads, JSON_PRETTY_PRINT) + ) !== false; } return true; } @@ -107,9 +108,10 @@ public static function dumpToDisk() : bool { /** * Load dumped known episodes into Redis (done on container startup) */ - public static function loadFromDisk() : array { - if( is_file(__DIR__ . '/../data/unread.json') ){ - $reads = json_decode(file_get_contents(__DIR__ . '/../data/unread.json'), true); + public static function loadFromDisk(?string $exportDir = null) : array { + $file = (is_null($exportDir) ? __DIR__ . '/../data' : $exportDir) . '/unread.json'; + if( is_file($file) ){ + $reads = json_decode(file_get_contents($file), true); foreach( $reads as $id => $read ){ if( !empty($read) ){ $redis = new Cache('unread_podcasts.' . $id ); diff --git a/php/classes/templates/main.json b/php/classes/templates/main.json index c30392b..d4d25a3 100644 --- a/php/classes/templates/main.json +++ b/php/classes/templates/main.json @@ -2,6 +2,5 @@ "%%MOREHEADER%%" : "", "%%TITLE%%": "", "%%INNERCONTAINER%%" : "", - "%%VERSION%%" : "unknown", "%%UPDATEINFO%%" : "style=\"display:none;\"" } \ No newline at end of file diff --git a/php/classes/templates/main_de.html b/php/classes/templates/main_de.html index 19e3af2..5a0930b 100644 --- a/php/classes/templates/main_de.html +++ b/php/classes/templates/main_de.html @@ -6,8 +6,8 @@