diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php
index 17da20b97..212cd7c8b 100644
--- a/airtime_mvc/application/controllers/ApiController.php
+++ b/airtime_mvc/application/controllers/ApiController.php
@@ -297,17 +297,23 @@ class ApiController extends Zend_Controller_Action
$stationUrl = Application_Common_HTTPHelper::getStationUrl();
- $previousID = $result["previous"]["metadata"]["id"];
- $get_prev_artwork_url = $stationUrl . 'api/track?id='. $previousID .'&return=artwork';
- $result["previous"]["metadata"]["artwork_url"] = $get_prev_artwork_url;
+ if ($result["previous"]["type"] != "livestream") {
+ $previousID = $result["previous"]["metadata"]["id"];
+ $get_prev_artwork_url = $stationUrl . 'api/track?id='. $previousID .'&return=artwork';
+ $result["previous"]["metadata"]["artwork_url"] = $get_prev_artwork_url;
+ }
- $currID = $result["current"]["metadata"]["id"];
- $get_curr_artwork_url = $stationUrl . 'api/track?id='. $currID .'&return=artwork';
- $result["current"]["metadata"]["artwork_url"] = $get_curr_artwork_url;
+ if ($result["current"]["type"] != "livestream") {
+ $currID = $result["current"]["metadata"]["id"];
+ $get_curr_artwork_url = $stationUrl . 'api/track?id='. $currID .'&return=artwork';
+ $result["current"]["metadata"]["artwork_url"] = $get_curr_artwork_url;
+ }
- $nextID = $result["previous"]["metadata"]["id"];
- $get_next_artwork_url = $stationUrl . 'api/track?id='. $nextID .'&return=artwork';
- $result["previous"]["metadata"]["artwork_url"] = $get_next_artwork_url;
+ if ($result["next"]["type"] != "livestream") {
+ $nextID = $result["next"]["metadata"]["id"];
+ $get_next_artwork_url = $stationUrl . 'api/track?id='. $nextID .'&return=artwork';
+ $result["next"]["metadata"]["artwork_url"] = $get_next_artwork_url;
+ }
// apply user-defined timezone, or default to station
Application_Common_DateHelper::convertTimestampsToTimezone(
diff --git a/airtime_mvc/application/controllers/ScheduleController.php b/airtime_mvc/application/controllers/ScheduleController.php
index bf9d0fa63..54482da8c 100644
--- a/airtime_mvc/application/controllers/ScheduleController.php
+++ b/airtime_mvc/application/controllers/ScheduleController.php
@@ -302,8 +302,10 @@ class ScheduleController extends Zend_Controller_Action
$range["previous"]["ends"] = Application_Common_DateHelper::UTCStringToUserTimezoneString($range["previous"]["ends"]);
}
if (isset($range["current"])) {
- $get_artwork = FileDataHelper::getArtworkData($range["current"]["metadata"]["artwork"], 256);
- $range["current"]["metadata"]["artwork_data"] = $get_artwork;
+ if (isset($range["current"]["metadata"])) {
+ $get_artwork = FileDataHelper::getArtworkData($range["current"]["metadata"]["artwork"], 256);
+ $range["current"]["metadata"]["artwork_data"] = $get_artwork;
+ }
$range["current"]["starts"] = Application_Common_DateHelper::UTCStringToUserTimezoneString($range["current"]["starts"]);
$range["current"]["ends"] = Application_Common_DateHelper::UTCStringToUserTimezoneString($range["current"]["ends"]);
}
diff --git a/airtime_mvc/build/sql/defaultdata.sql b/airtime_mvc/build/sql/defaultdata.sql
index cd976b11f..82b27c1e0 100644
--- a/airtime_mvc/build/sql/defaultdata.sql
+++ b/airtime_mvc/build/sql/defaultdata.sql
@@ -32,7 +32,7 @@ INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_host', '1
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_port', '8000', 'integer');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_user', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_pass', 'hackme', 'string');
-INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_admin_user', '', 'string');
+INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_admin_user', 'admin', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_admin_pass', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_mount', 'airtime_128', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s1_url', 'https://libretime.org', 'string');
@@ -47,7 +47,7 @@ INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_host', ''
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_port', '', 'integer');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_user', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_pass', '', 'string');
-INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_admin_user', '', 'string');
+INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_admin_user', 'admin', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_admin_pass', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_mount', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s2_url', '', 'string');
@@ -62,7 +62,7 @@ INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_host', ''
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_port', '', 'integer');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_user', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_pass', '', 'string');
-INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_admin_user', '', 'string');
+INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_admin_user', 'admin', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_admin_pass', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_mount', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_url', '', 'string');
@@ -370,7 +370,7 @@ INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_host', ''
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_port', '', 'integer');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_user', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_pass', '', 'string');
-INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_admin_user', '', 'string');
+INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_admin_user', 'admin', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_admin_pass', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_mount', '', 'string');
INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s4_url', '', 'string');
diff --git a/airtime_mvc/public/js/airtime/dashboard/dashboard.js b/airtime_mvc/public/js/airtime/dashboard/dashboard.js
index 1b4d1c468..555cc4c7b 100644
--- a/airtime_mvc/public/js/airtime/dashboard/dashboard.js
+++ b/airtime_mvc/public/js/airtime/dashboard/dashboard.js
@@ -130,8 +130,7 @@ function updatePlaybar(){
$('#current').html(""+$.i18n._("Recording:")+""+currentSong.name+",");
} else {
$('#current').text(currentSong.name+",");
-
- if (currentSong.metadata.artwork_data) {
+ if (currentSong.metadata && currentSong.metadata.artwork_data) {
var check_current_song = Cookies.get('current_track');
var loaded = Cookies.get('loaded');
diff --git a/airtime_mvc/public/setup/database-setup.php b/airtime_mvc/public/setup/database-setup.php
index 17038ac74..a3e3ff2b6 100644
--- a/airtime_mvc/public/setup/database-setup.php
+++ b/airtime_mvc/public/setup/database-setup.php
@@ -79,6 +79,7 @@ class DatabaseSetup extends Setup {
$this->setNewDatabaseConnection(self::$_properties["dbname"]);
$this->checkSchemaExists();
$this->createDatabaseTables();
+ $this->updateIcecastPassword();
}
/**
@@ -175,5 +176,82 @@ class DatabaseSetup extends Setup {
array(self::DB_NAME,));
}
}
+ /**
+ * Updates the icecast password in the database based upon the temp file created during install
+ * @throws AirtimeDatabaseException
+ */
+ private function updateIcecastPassword() {
+ if (!file_exists(LIBRETIME_CONF_DIR . '/icecast_pass')) {
+ throw new AirtimeDatabaseException("The Icecast Password file was not accessible", array());
+ };
+ $icecast_pass_txt = file(LIBRETIME_CONF_DIR . '/icecast_pass');
+ $icecast_pass = $icecast_pass_txt[0];
+ $icecast_pass = str_replace(PHP_EOL, '', $icecast_pass);
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_admin_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's2_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's2_admin_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's3_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's3_admin_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ $statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_admin_pass'");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ $statement = self::$dbh->prepare("INSERT INTO cc_pref (keystr, valstr) VALUES ('default_icecast_password', :icecastpass )");
+ $statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
+ try {
+ $statement->execute();
+ }
+ catch (PDOException $ex) {
+ print "Error!: " . $ex->getMessage() . "
";
+ }
+ }
}
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
new file mode 100644
index 000000000..6f382aba2
--- /dev/null
+++ b/docs/api/openapi.yaml
@@ -0,0 +1,1343 @@
+---
+openapi: "3.0.0"
+info:
+ title: LibreTime API overview
+ version: 1.1
+paths:
+ /live-info:
+ get:
+ summary: Retrieve the currently playing show as well as upcoming shows
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: type
+ in: path
+ description: |-
+ endofday retrieves the info for shows up until the end of the day,
+ while interval will return shows in the next 48 hours
+ schema:
+ enum:
+ - 'endofday'
+ - 'interval'
+ default: 'interval'
+ required: false
+ - name: limit
+ in: path
+ description: the number of shows to retrieve
+ schema:
+ type: integer
+ default: 5
+ required: false
+ responses:
+ '200':
+ description: 200 response for default request
+ content:
+ application/json:
+ example: {
+ "env": "production",
+ "schedulerTime": "2019-10-21 17:52:45",
+ "previous": {
+ "starts": "2019-10-21 17:47:25.000000",
+ "ends": "2019-10-21 17:52:13.000000",
+ "type": "track",
+ "name": "Disclosure - F For You (feat. Mary J. Blige)",
+ "metadata": {
+ "id": 8,
+ "name": "",
+ "mime": "audio/mp3",
+ "ftype": "audioclip",
+ "directory": 1,
+ "filepath": "imported/1/Disclosure/www.mmibty.com/01-F-For-You-feat.-Mary-J.-Blige.mp3",
+ "import_status": 0,
+ "currentlyaccessing": 0,
+ "editedby": null,
+ "mtime": "2019-10-21 17:19:03",
+ "utime": "2019-10-21 17:18:57",
+ "lptime": "2019-10-21 17:47:25",
+ "md5": "e008616551750aea49820a16d1fb1527",
+ "track_title": "F For You (feat. Mary J. Blige)",
+ "artist_name": "Disclosure",
+ "bit_rate": 251628,
+ "sample_rate": 44100,
+ "format": null,
+ "length": "00:04:48.026122",
+ "album_title": "www.mmibty.com",
+ "genre": "Electronic",
+ "comments": null,
+ "year": "2014",
+ "track_number": 1,
+ "channels": 2,
+ "url": null,
+ "bpm": null,
+ "rating": null,
+ "encoded_by": null,
+ "disc_number": null,
+ "mood": null,
+ "label": null,
+ "composer": null,
+ "encoder": null,
+ "checksum": null,
+ "lyrics": null,
+ "orchestra": null,
+ "conductor": null,
+ "lyricist": null,
+ "original_lyricist": null,
+ "radio_station_name": null,
+ "info_url": null,
+ "artist_url": null,
+ "audio_source_url": null,
+ "radio_station_url": null,
+ "buy_this_url": null,
+ "isrc_number": null,
+ "catalog_number": null,
+ "original_artist": null,
+ "copyright": null,
+ "report_datetime": null,
+ "report_location": null,
+ "report_organization": null,
+ "subject": null,
+ "contributor": null,
+ "language": null,
+ "soundcloud_id": null,
+ "soundcloud_error_code": null,
+ "soundcloud_error_msg": null,
+ "soundcloud_link_to_file": null,
+ "soundcloud_upload_time": null,
+ "replay_gain": "-5.58",
+ "owner_id": 1,
+ "cuein": "00:00:00",
+ "cueout": "00:04:48.026122",
+ "hidden": false,
+ "filesize": 9271626,
+ "description": null,
+ "artwork": "imported/1/artwork/01-F-For-You-feat.-Mary-J.-Blige",
+ "artwork_url": "http://localhost:8080/api/track?id=8&return=artwork"
+ }
+ },
+ "current": {
+ "starts": "2019-10-21 17:52:13",
+ "ends": "2019-10-21 17:56:27",
+ "type": "track",
+ "name": "Armin van Buuren - Ping Pong",
+ "media_item_played": true,
+ "metadata": {
+ "id": 2,
+ "name": "",
+ "mime": "audio/mp3",
+ "ftype": "audioclip",
+ "directory": 1,
+ "filepath": "imported/1/Armin van Buuren/A State of Trance 2014/2-18 Armin van Buuren - Ping Pong.mp3",
+ "import_status": 0,
+ "currentlyaccessing": 0,
+ "editedby": null,
+ "mtime": "2019-10-21 17:18:02",
+ "utime": "2019-10-21 17:18:00",
+ "lptime": "2019-10-21 17:52:13",
+ "md5": "04c26823902065db0706d121d0e703a2",
+ "track_title": "Ping Pong",
+ "artist_name": "Armin van Buuren",
+ "bit_rate": 32000,
+ "sample_rate": 44100,
+ "format": null,
+ "length": "00:04:14.171429",
+ "album_title": "A State of Trance 2014",
+ "genre": "Trance;Electronic;Dance",
+ "comments": null,
+ "year": "2014",
+ "track_number": 18,
+ "channels": 2,
+ "url": null,
+ "bpm": null,
+ "rating": null,
+ "encoded_by": null,
+ "disc_number": null,
+ "mood": null,
+ "label": "Armada Music",
+ "composer": null,
+ "encoder": null,
+ "checksum": null,
+ "lyrics": null,
+ "orchestra": null,
+ "conductor": null,
+ "lyricist": null,
+ "original_lyricist": null,
+ "radio_station_name": null,
+ "info_url": null,
+ "artist_url": null,
+ "audio_source_url": null,
+ "radio_station_url": null,
+ "buy_this_url": null,
+ "isrc_number": null,
+ "catalog_number": null,
+ "original_artist": null,
+ "copyright": null,
+ "report_datetime": null,
+ "report_location": null,
+ "report_organization": null,
+ "subject": null,
+ "contributor": null,
+ "language": null,
+ "soundcloud_id": null,
+ "soundcloud_error_code": null,
+ "soundcloud_error_msg": null,
+ "soundcloud_link_to_file": null,
+ "soundcloud_upload_time": null,
+ "replay_gain": "-5.07",
+ "owner_id": 1,
+ "cuein": "00:00:00",
+ "cueout": "00:04:14.171429",
+ "hidden": false,
+ "filesize": 6136238,
+ "description": null,
+ "artwork": "imported/1/artwork/2-18 Armin van Buuren - Ping Pong",
+ "artwork_url": "http://localhost:8080/api/track?id=2&return=artwork"
+ },
+ "record": "0"
+ },
+ "next": {
+ "starts": "2019-10-21 17:56:27.000000",
+ "ends": "2019-10-21 18:00:28.000000",
+ "type": "track",
+ "name": "Bastille - No Angels (feat. Ella)",
+ "metadata": {
+ "id": 4,
+ "name": "",
+ "mime": "audio/mp3",
+ "ftype": "audioclip",
+ "directory": 1,
+ "filepath": "imported/1/Bastille/Other People's Heartache, Pt. 2/03 Bastille - No Angels (feat. Ella).mp3",
+ "import_status": 0,
+ "currentlyaccessing": 0,
+ "editedby": null,
+ "mtime": "2019-10-21 17:18:16",
+ "utime": "2019-10-21 17:18:14",
+ "lptime": "2019-10-21 17:24:46",
+ "md5": "87bf83451d7618eefc0141c262aead2a",
+ "track_title": "No Angels (feat. Ella)",
+ "artist_name": "Bastille",
+ "bit_rate": 128000,
+ "sample_rate": 44100,
+ "format": null,
+ "length": "00:04:00.752438",
+ "album_title": "Other People's Heartache, Pt. 2",
+ "genre": null,
+ "comments": null,
+ "year": "2012",
+ "track_number": 3,
+ "channels": 2,
+ "url": null,
+ "bpm": null,
+ "rating": null,
+ "encoded_by": null,
+ "disc_number": null,
+ "mood": null,
+ "label": "[no label]",
+ "composer": null,
+ "encoder": null,
+ "checksum": null,
+ "lyrics": null,
+ "orchestra": null,
+ "conductor": null,
+ "lyricist": null,
+ "original_lyricist": null,
+ "radio_station_name": null,
+ "info_url": null,
+ "artist_url": null,
+ "audio_source_url": null,
+ "radio_station_url": null,
+ "buy_this_url": null,
+ "isrc_number": null,
+ "catalog_number": null,
+ "original_artist": null,
+ "copyright": null,
+ "report_datetime": null,
+ "report_location": null,
+ "report_organization": null,
+ "subject": null,
+ "contributor": null,
+ "language": null,
+ "soundcloud_id": null,
+ "soundcloud_error_code": null,
+ "soundcloud_error_msg": null,
+ "soundcloud_link_to_file": null,
+ "soundcloud_upload_time": null,
+ "replay_gain": "-8.57",
+ "owner_id": 1,
+ "cuein": "00:00:00",
+ "cueout": "00:04:00.752438",
+ "hidden": false,
+ "filesize": 3858688,
+ "description": null,
+ "artwork": ""
+ }
+ },
+ "currentShow": [
+ {
+ "start_timestamp": "2019-10-21 17:20:00",
+ "end_timestamp": "2019-10-21 18:31:00",
+ "name": "Show 1",
+ "description": "A show",
+ "id": 1,
+ "instance_id": 1,
+ "record": 0,
+ "url": "https://example.com",
+ "image_path": "",
+ "starts": "2019-10-21 17:20:00",
+ "ends": "2019-10-21 18:31:00"
+ }
+ ],
+ "nextShow": [
+ {
+ "id": 2,
+ "instance_id": 2,
+ "name": "Reading",
+ "description": "A reading of After the EMP by Harley Tate",
+ "url": "https://example.com",
+ "start_timestamp": "2019-10-21 18:31:00",
+ "end_timestamp": "2019-10-22 10:45:00",
+ "starts": "2019-10-21 18:31:00",
+ "ends": "2019-10-22 10:45:00",
+ "record": 0,
+ "image_path": "",
+ "type": "show"
+ }
+ ],
+ "source_enabled": "Scheduled",
+ "timezone": "UTC",
+ "timezoneOffset": "0",
+ "AIRTIME_API_VERSION": "1.1"
+ }
+ /live-info-v2:
+ get:
+ summary: Retrieve the currently playing and upcoming shows
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ - name: days
+ in: path
+ description: The number of days to return
+ schema:
+ type: integer
+ default: 2
+ required: false
+ - show_id: shows
+ in: path
+ description: the number of shows to retrieve
+ schema:
+ type: integer
+ default: 5
+ required: false
+ - name: timezone
+ in: path
+ description: The timezone to send the times in
+ schema:
+ type: string
+ default: "$server_timezone"
+ required: false
+ responses:
+ '200':
+ description: 200 response for default request
+ content:
+ application/json:
+ example: {
+ "station": {
+ "env": "production",
+ "schedulerTime": "2019-10-21 17:29:40",
+ "source_enabled": "Scheduled",
+ "timezone": "UTC",
+ "AIRTIME_API_VERSION": "1.1"
+ },
+ "tracks": {
+ "previous": {
+ "starts": "2019-10-21 17:24:45",
+ "ends": "2019-10-21 17:28:46",
+ "type": "track",
+ "name": "Bastille - No Angels (feat. Ella)",
+ "metadata": {
+ "id": 4,
+ "name": "",
+ "mime": "audio/mp3",
+ "ftype": "audioclip",
+ "directory": 1,
+ "filepath": "imported/1/Bastille/Other People's Heartache, Pt. 2/03 Bastille - No Angels (feat. Ella).mp3",
+ "import_status": 0,
+ "currentlyaccessing": 0,
+ "editedby": null,
+ "mtime": "2019-10-21 17:18:16",
+ "utime": "2019-10-21 17:18:14",
+ "lptime": "2019-10-21 17:24:46",
+ "md5": "87bf83451d7618eefc0141c262aead2a",
+ "track_title": "No Angels (feat. Ella)",
+ "artist_name": "Bastille",
+ "bit_rate": 128000,
+ "sample_rate": 44100,
+ "format": null,
+ "length": "00:04:00.752438",
+ "album_title": "Other People's Heartache, Pt. 2",
+ "genre": null,
+ "comments": null,
+ "year": "2012",
+ "track_number": 3,
+ "channels": 2,
+ "url": null,
+ "bpm": null,
+ "rating": null,
+ "encoded_by": null,
+ "disc_number": null,
+ "mood": null,
+ "label": "[no label]",
+ "composer": null,
+ "encoder": null,
+ "checksum": null,
+ "lyrics": null,
+ "orchestra": null,
+ "conductor": null,
+ "lyricist": null,
+ "original_lyricist": null,
+ "radio_station_name": null,
+ "info_url": null,
+ "artist_url": null,
+ "audio_source_url": null,
+ "radio_station_url": null,
+ "buy_this_url": null,
+ "isrc_number": null,
+ "catalog_number": null,
+ "original_artist": null,
+ "copyright": null,
+ "report_datetime": null,
+ "report_location": null,
+ "report_organization": null,
+ "subject": null,
+ "contributor": null,
+ "language": null,
+ "soundcloud_id": null,
+ "soundcloud_error_code": null,
+ "soundcloud_error_msg": null,
+ "soundcloud_link_to_file": null,
+ "soundcloud_upload_time": null,
+ "replay_gain": "-8.57",
+ "owner_id": 1,
+ "cuein": "00:00:00",
+ "cueout": "00:04:00.752438",
+ "hidden": false,
+ "filesize": 3858688,
+ "description": null,
+ "artwork": ""
+ }
+ },
+ "current": null,
+ "next": {
+ "starts": "2019-10-21 17:32:49",
+ "ends": "2019-10-21 17:36:44",
+ "type": "track",
+ "name": "Bob Marley - Could You Be Loved",
+ "metadata": {
+ "id": 14,
+ "name": "",
+ "mime": "audio/mp3",
+ "ftype": "audioclip",
+ "directory": 1,
+ "filepath": "imported/1/Bob Marley/Greatest Hits/02. Could You Be Loved.mp3",
+ "import_status": 0,
+ "currentlyaccessing": 0,
+ "editedby": null,
+ "mtime": "2019-10-21 17:19:16",
+ "utime": "2019-10-21 17:18:59",
+ "lptime": null,
+ "md5": "75e49569fd6af61cc8c18f5660beadc2",
+ "track_title": "Could You Be Loved",
+ "artist_name": "Bob Marley",
+ "bit_rate": 128000,
+ "sample_rate": 44100,
+ "format": null,
+ "length": "00:03:55.11",
+ "album_title": "Greatest Hits",
+ "genre": "Various",
+ "comments": null,
+ "year": null,
+ "track_number": 2,
+ "channels": 2,
+ "url": null,
+ "bpm": 103,
+ "rating": null,
+ "encoded_by": null,
+ "disc_number": null,
+ "mood": null,
+ "label": null,
+ "composer": null,
+ "encoder": null,
+ "checksum": null,
+ "lyrics": null,
+ "orchestra": null,
+ "conductor": null,
+ "lyricist": null,
+ "original_lyricist": null,
+ "radio_station_name": null,
+ "info_url": null,
+ "artist_url": null,
+ "audio_source_url": null,
+ "radio_station_url": null,
+ "buy_this_url": null,
+ "isrc_number": null,
+ "catalog_number": null,
+ "original_artist": null,
+ "copyright": null,
+ "report_datetime": null,
+ "report_location": null,
+ "report_organization": null,
+ "subject": null,
+ "contributor": null,
+ "language": null,
+ "soundcloud_id": null,
+ "soundcloud_error_code": null,
+ "soundcloud_error_msg": null,
+ "soundcloud_link_to_file": null,
+ "soundcloud_upload_time": null,
+ "replay_gain": "-1.2",
+ "owner_id": 1,
+ "cuein": "00:00:00",
+ "cueout": "00:03:55.11",
+ "hidden": false,
+ "filesize": 3773820,
+ "description": null,
+ "artwork": ""
+ }
+ }
+ },
+ "shows": {
+ "previous": [],
+ "current": {
+ "name": "Show 1",
+ "description": "A show",
+ "genre": "HipHop",
+ "id": 1,
+ "instance_id": 1,
+ "record": 0,
+ "url": "https://example.com",
+ "image_path": "",
+ "starts": "2019-10-21 17:20:00",
+ "ends": "2019-10-21 18:31:00"
+ },
+ "next": [
+ {
+ "name": "Reading",
+ "description": "A reading of After the EMP by Harley Tate",
+ "genre": "Sci-fi",
+ "id": 2,
+ "instance_id": 2,
+ "record": 0,
+ "url": "https://example.com",
+ "image_path": "",
+ "starts": "2019-10-21 18:31:00",
+ "ends": "2019-10-22 10:45:00"
+ }
+ ]
+ },
+ "sources": {
+ "livedj": "off",
+ "masterdj": "off",
+ "scheduledplay": "on"
+ }
+ }
+ /week-info:
+ get:
+ summary: Retrieve the schedule for the week
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ responses:
+ '200':
+ description: 200 response for default request
+ content:
+ application/json:
+ example: {
+ "monday": [
+ {
+ "start_timestamp": "2019-10-21 17:20:00",
+ "end_timestamp": "2019-10-21 18:31:00",
+ "name": "Show 1",
+ "description": "A show",
+ "id": 1,
+ "instance_id": 1,
+ "instance_description": "",
+ "record": 0,
+ "url": "https://example.com",
+ "image_path": "",
+ "starts": "2019-10-21 17:20:00",
+ "ends": "2019-10-21 18:31:00"
+ },
+ {
+ "start_timestamp": "2019-10-21 18:31:00",
+ "end_timestamp": "2019-10-22 10:45:00",
+ "name": "Reading",
+ "description": "A reading of After the EMP by Harley Tate",
+ "id": 2,
+ "instance_id": 2,
+ "instance_description": "",
+ "record": 0,
+ "url": "https://example.com",
+ "image_path": "",
+ "starts": "2019-10-21 18:31:00",
+ "ends": "2019-10-22 10:45:00"
+ }
+ ],
+ "tuesday": [],
+ "wednesday": [],
+ "thursday": [],
+ "friday": [],
+ "saturday": [],
+ "sunday": [],
+ "nextmonday": [],
+ "nexttuesday": [],
+ "nextwednesday": [],
+ "nextthursday": [],
+ "nextfriday": [],
+ "nextsaturday": [],
+ "nextsunday": [],
+ "AIRTIME_API_VERSION": "1.1"
+ }
+ /station-metadata:
+ get:
+ summary: BROKEN - Retrieve the schedule for the week
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ /station-logo:
+ get:
+ summary: Fetch the station logo
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ /show-history-feed:
+ get:
+ summary: BROKEN - Retrieve the show shedules for a given time range and show
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: start
+ in: path
+ description: The start time for the feed
+ required: true
+ - name: end
+ in: path
+ description: The end time for the feed
+ required: true
+ - name: timezone
+ in: path
+ description: The timezone that the times are in
+ required: true
+ /item-history-feed:
+ get:
+ summary: Retrieve the items for a time range and/or show
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: start
+ in: path
+ description: The start time for the feed
+ required: false
+ - name: end
+ in: path
+ description: The end time for the feed
+ required: false
+ - name: timezone
+ in: path
+ description: The timezone that the times are in
+ required: false
+ - name: instance_id
+ in: path
+ description: The show instance ID
+ required: false
+ responses:
+ '200':
+ description: The 200 default response
+ content:
+ application/json:
+ example: [
+ {
+ "starts": "2019-10-21 18:19:07",
+ "ends": "2019-10-21 18:23:55",
+ "history_id": 16,
+ "instance_id": 1,
+ "track_title": "F For You (feat. Mary J. Blige)",
+ "artist_name": "Disclosure",
+ "checkbox": ""
+ },
+ {
+ "starts": "2019-10-21 17:20:31",
+ "ends": "2019-10-21 17:24:45",
+ "history_id": 1,
+ "instance_id": 1,
+ "track_title": "Ping Pong",
+ "artist_name": "Armin van Buuren",
+ "checkbox": ""
+ },
+ ]
+ /shows:
+ get:
+ summary: Retrieve the show info (without schedule for given show_id
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: show_id
+ in: path
+ description: The ID of the show
+ required: false
+ response:
+ '200':
+ description: The response with a show_id of 1
+ content:
+ application/json:
+ example: [
+ {
+ "name": "Show 1",
+ "id": 1,
+ "url": "https://example.com",
+ "genre": "HipHop",
+ "description": "A show",
+ "color": "",
+ "background_color": "",
+ "linked": false,
+ "has_autoplaylist": false,
+ "autoplaylist_id": null,
+ "autoplaylist_repeat": false
+ }
+ ]
+ /show-tracks:
+ get:
+ summary: Display the track listing for given instance_id
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: instance_id
+ in: path
+ description: The ID of the show
+ required: true
+ response:
+ '200':
+ description: The response with a instance_id of 1
+ content:
+ application/json:
+ example: [
+ {
+ "title": "Ping Pong",
+ "artist": "Armin van Buuren",
+ "position": 0,
+ "id": 1,
+ "mime": "audio/mp3",
+ "starts": "2019-10-21 17:20:31",
+ "length": "4:14.2",
+ "file_id": 2
+ },
+ {
+ "title": "No Angels (feat. Ella)",
+ "artist": "Bastille",
+ "position": 1,
+ "id": 2,
+ "mime": "audio/mp3",
+ "starts": "2019-10-21 17:24:45",
+ "length": "4:00.8",
+ "file_id": 4
+ }
+ ]
+ /show-schedules:
+ get:
+ summary: Display the show schedule for given show_id
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: show_id
+ in: path
+ description: The ID of the show
+ required: true
+ - name: start
+ in: path
+ description: The start time for the feed
+ required: false
+ - name: end
+ in: path
+ description: The end time for the feed
+ required: false
+ - name: timezone
+ in: path
+ description: The timezone that the times are in
+ required: false
+ response:
+ '200':
+ description: The response with a instance_id of 1
+ content:
+ application/json:
+ example: [
+ {
+ "starts": "2019-10-21 17:20:00",
+ "ends": "2019-10-21 18:31:00",
+ "record": 0,
+ "rebroadcast": 0,
+ "parent_starts": null,
+ "record_id": null,
+ "show_id": 1,
+ "name": "Show 1",
+ "description": "A show",
+ "color": "",
+ "background_color": "",
+ "image_path": "",
+ "linked": false,
+ "file_id": null,
+ "instance_id": 1,
+ "instance_description": "",
+ "created": "2019-10-21 17:20:22",
+ "last_scheduled": "2019-10-21 17:20:50",
+ "time_filled": "01:14:39.265872",
+ "soundcloud_id": null
+ }
+ ]
+ /show-logo:
+ get:
+ summary: Fetch the show logo. Returns the station logo if none exists
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: id
+ in: path
+ description: The ID of the show with the logo to retrieve
+ required: true
+ /track:
+ get:
+ summary: Displays the metadata of a particular track
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ - name: id
+ in: path
+ description: The track id
+ required: true
+ - name: response
+ in: path
+ description: The type of response
+ schema:
+ enum:
+ - json
+ - artwork_data
+ - artwork
+ required: true
+ responses:
+ '200':
+ description: The 200 response
+ content:
+ application/json:
+ example: {
+ "MDATA_KEY_FILEPATH": "imported\/1\/Armin van Buuren\/Another You (feat. Mr. Probz)\/01 Another You (feat. Mr. Probz).mp3",
+ "MDATA_KEY_DIRECTORY": 1,
+ "MDATA_KEY_TITLE": "Another You (feat. Mr. Probz)",
+ "MDATA_KEY_CREATOR": "Armin van Buuren",
+ "MDATA_KEY_SOURCE": "Another You (feat. Mr. Probz)",
+ "MDATA_KEY_DURATION": "00:03:19.183673",
+ "MDATA_KEY_MIME": "audio\/mp3",
+ "MDATA_KEY_FTYPE": "audioclip",
+ "MDATA_KEY_URL": null,
+ "MDATA_KEY_GENRE": null,
+ "MDATA_KEY_MOOD": null,
+ "MDATA_KEY_LABEL": "Armin Audio B.V.",
+ "MDATA_KEY_COMPOSER": null,
+ "MDATA_KEY_DESCRIPTION": null,
+ "MDATA_KEY_SAMPLERATE": 44100,
+ "MDATA_KEY_BITRATE": 192000,
+ "MDATA_KEY_ENCODER": null,
+ "MDATA_KEY_ISRC": null,
+ "MDATA_KEY_COPYRIGHT": null,
+ "MDATA_KEY_YEAR": "2015",
+ "MDATA_KEY_BPM": null,
+ "MDATA_KEY_TRACKNUMBER": 1,
+ "MDATA_KEY_CONDUCTOR": null,
+ "MDATA_KEY_LANGUAGE": null,
+ "MDATA_KEY_REPLAYGAIN": "-8.36",
+ "MDATA_KEY_OWNER_ID": 1,
+ "MDATA_KEY_CUE_IN": "00:00:00",
+ "MDATA_KEY_CUE_OUT": "00:03:19.183673",
+ "MDATA_KEY_ARTWORK": "imported\/1\/artwork\/01 Another You (feat. Mr. Probz)"
+ }
+ /stream-m3u:
+ get:
+ summary: Returns m3u playlist file for the station's output stream
+ response:
+ '200':
+ description: The M3U file for the stream
+ content: application/x-mpegurl
+ /version:
+ get:
+ summary: Returns the current LibreTime and API versions
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: false
+ responses:
+ '200':
+ description: 200 response
+ content:
+ application/json:
+ example: {
+ "airtime_version": "3.0.0~alpha.5",
+ "api_version": "1.1"
+ }
+ /recorded-shows:
+ get:
+ summary: BROKEN - Unclear what this did, not implemented in ApiController
+ /calendar-init:
+ get:
+ summary: BROKEN - Unclear what this did, not implemented in ApiController
+ /upload-file:
+ get:
+ summary: BROKEN - Unclear what this did, not implemented in ApiController
+ /upload-recorded:
+ post:
+ summary: Upload a recorded show
+ parameters:
+ - name: showinstanceid
+ in: path
+ description: The ID of the show that was recorded
+ required: true
+ - name: fileid
+ in: path
+ description: The ID of the recorded file in the database
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /media-monitor-setup:
+ post:
+ summary: Initialises monitoring of media directories
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /media-item-status:
+ get:
+ summary: BROKEN - Unclear what this did, not implemented in ApiController
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /reload-metadata:
+ get:
+ summary: BROKEN - Unclear what this did, not implemented in ApiController
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /list-all-files:
+ get:
+ summary: BROKEN - List all files in a given directory managed by LibreTime
+ parameters:
+ - name: dir_id
+ in: path
+ description: The directory to list files in
+ required: true
+ - name: all
+ in: path
+ description: |-
+ true to show all files in the database, even if they do not exist
+ on disk
+ required: false
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /list-all-watched-dirs:
+ get:
+ summary: |-
+ BROKEN (LT does not currently support watched files) - lists all
+ directories to watch for new files
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /add-watched-dir:
+ post:
+ summary: |-
+ BROKEN (LT does not currently support watched files) - adds a directory
+ to the list of watched directories
+ parameters:
+ - name: path
+ in: path
+ description: the path of the directory on the server, base64 encoded
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /remove-watched-dir:
+ post:
+ summary: |-
+ BROKEN (LT does not currently support watched files) - removes a
+ directory from the list of watched directories
+ parameters:
+ - name: path
+ in: path
+ description: the path of the directory on the server, base64 encoded
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /set-storage-dir:
+ post:
+ summary: Sets the storage path for music files
+ parameters:
+ - name: path
+ in: path
+ description: The base64 encoded path to the new storage directory
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /get-stream-setting:
+ get:
+ summary: |-
+ BROKEN - Returns the settings configured for the four output Icecast
+ streams
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /status:
+ get:
+ summary: |-
+ BROKEN - Returns the current status of the various LibreTime
+ components
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /register-component:
+ post:
+ summary: Add a service component (for example Monit)
+ parameters:
+ - name: component
+ in: path
+ description: The component name to add
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /update-liquidsoap-status:
+ post:
+ summary: |-
+ Update the status of a stream to indicate a Liquidsoap status update
+ parameters:
+ - name: msg_post
+ in: path
+ description: The error message to use
+ required: true
+ - name: stream_id
+ in: path
+ description: The ID of the stream to update
+ required: true
+ - name: boot_time
+ description: |-
+ The time that the status was last updated. Excluding this will
+ force update.
+ required: false
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /update-file-system-mount:
+ post:
+ summary: |-
+ Handles additions/deletions of mount points on which watched
+ directories reside
+ parameters:
+ - name: added_dir
+ in: path
+ description: |-
+ A comma separated list of directories that were added to the system
+ required: false
+ - name: removed_dir
+ in: path
+ description: |-
+ A comma separated list of directories that were removed from the
+ system
+ required: false
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /handle-watched-dir-missing:
+ post:
+ summary: |-
+ BROKEN (LibreTime does not currentl handle watched directories) -
+ Handles missing watched directories.
+ parameters:
+ - name: dir
+ in: path
+ description: The directory to disable
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /rabbitmq-do-push:
+ post:
+ summary: |-
+ Used by dev scripts to make rabbitmq send out a message to pypo that a
+ potential change has been made to the database.
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /check-live-stream-auth:
+ get:
+ summary: |-
+ Tests the authentication supplied to authenticate DJs connecting to a
+ live stream
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ username:
+ type: string
+ password:
+ type: string
+ format: password
+ djtype:
+ type: string
+ required:
+ - username
+ - password
+ - djtype
+ /update-source-status:
+ post:
+ summary: Update on source connect or disconnect
+ parameters:
+ - name: sourcename
+ in: path
+ description: |-
+ The name of the source that has connected or disconnected
+ - name: status
+ in: path
+ description: true if the source is now connected
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /get-bootstrap-info:
+ get:
+ summary: |-
+ BROKEN - Retrieves the current state of the instance. This includes
+ which sources are currently connected, station name, etc
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /get-files-without-replay-gain:
+ get:
+ summary: |-
+ BROKEN (returns all files) - Returns the files that do not have replay
+ gain set.
+ parameters:
+ - name: dir_id
+ in: path
+ description: The directory ID
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /get-files-without-silan-value:
+ get:
+ summary: Returns the files that have not been processed by silan
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ responses:
+ '200':
+ description: 200 response for default request
+ content:
+ application/json:
+ example: [
+ {
+ "id": 9,
+ "fp": "/srv/airtime/stor/"
+ },
+ {
+ "id": 12,
+ "fp": "/srv/airtime/stor/imported/1/Sam Smith Feat John Legend/The Official Uk Top 40 Singles Chart 03-22-2015/01 Sam Smith Feat John Legend - Lay Me Down.mp3"
+ },
+ {
+ "id": 13,
+ "fp": "/srv/airtime/stor/imported/1/Mumford & Sons/Wilder Mind [ Deluxe Edition ]/01 - Tompkins Square Park.mp3"
+ },
+ {
+ "id": 3,
+ "fp": "/srv/airtime/stor/imported/1/Bastille/All This Bad Blood/1-02 Things We Lost in the Fire.mp3"
+ },
+ {
+ "id": 1,
+ "fp": "/srv/airtime/stor/imported/1/Armin van Buuren/Another You (feat. Mr. Probz)/01 Another You (feat. Mr. Probz).mp3"
+ },
+ {
+ "id": 15,
+ "fp": "/srv/airtime/stor/imported/1/Harley Tate/Harley Tate - After the EMP 01 - After the EMP/Harley Tate - After the EMP 01 - After the EMP.mp3"
+ }
+ ]
+ /reload-metadata-group:
+ get:
+ summary: |-
+ Extracts all file metadata from the list of files
+ parameters:
+ - name: mdXXX
+ # This could be wrong - the function in ApiController is confusing...
+ in: path
+ description: |-
+ A json encoded hash with all the information related to the action.
+ the XXX represents at least 1 digit. Currently the mdXXX key has no
+ meaning.
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /notify-webstream-data:
+ post:
+ summary: Notifies webstreams of data being updated
+ parameters:
+ - name: data
+ in: path
+ description: |-
+ A json encoded hash with the data used to notify the webstream. This
+ includes keys such as 'title'.
+ required: true
+ - name: media_id
+ in: path
+ description: The ID of the media to show
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /get-stream-parameters:
+ get:
+ summary: BROKEN - Retrieves the parameters set for each stream
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /push-stream-stats:
+ post:
+ summary: Updates the listener number statistics
+ parameters:
+ - name: data
+ in: path
+ description: A json encoded mapping of time to number of listeners
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /update-stream-setting-table:
+ post:
+ summary: Set stream settings
+ parameters:
+ - name: data
+ in: path
+ description: A json encoded array of key-value pairs to update
+ required: true
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /update-replay-gain-value:
+ post:
+ summary: Updates the replay gain values for media items
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ - name: data
+ description: A json encoded array of media ID and gain pairs
+ required: true
+ /update-cue-values-by-silan:
+ post:
+ summary: Updates the silan cue values for media items
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ - name: data
+ description: A json encoded array of media ID and cue pairs
+ required: true
+ /get-usability-hint:
+ get:
+ summary: Returns the usibility hint tool-tip for a UI item
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ - name: userPath
+ in: path
+ description: The URL path to the UI item
+ required: true
+ /poll-celery:
+ post:
+ summary: Polls celery for tasks
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
+ /recalculate-schedule:
+ post:
+ summary: Recalculates the schedule to remove gaps and update timing
+ parameters:
+ - name: api_key
+ in: path
+ description: The API key to use for authentication
+ required: true
diff --git a/docs/install.md b/docs/install.md
index 201334b6d..c0c3e5c1d 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -45,5 +45,5 @@ Plans are in the works for `.rpm` packages, as well as Docker and AWS images.
Please note that the install script does not take care to ensure that any
packages installed are set up in a secure manner. Please see the chapter on
-[preparing the server](manual/preparing-the-server.md) for more details on
+[preparing the server](manual/preparing-the-server) for more details on
how to set up a secure installation.
diff --git a/docs/manual/general/index.md b/docs/manual/general/index.md
index d30c706af..c4dca1e11 100644
--- a/docs/manual/general/index.md
+++ b/docs/manual/general/index.md
@@ -50,7 +50,9 @@ wish. (There is more about this feature in the
*Advanced Configuration* section of this book).
The **Allowed CORS URLs** is intended to deal with situations where you want a
-remote site with a different domain to access the API.
+remote site with a different domain to access the API. This is relevant when
+there is a reverse proxy server in front of LibreTime. If you are using a
+reverse proxy, the URLs that will be used to access it should be added here.
The **Display login button on your Radio Page?** will determine whether visitors
to your site see a link to login. If this is disabled DJs and admins will need
diff --git a/docs/manual/getting-started/index.md b/docs/manual/getting-started/index.md
index 7a80a6a52..dc6871b6c 100644
--- a/docs/manual/getting-started/index.md
+++ b/docs/manual/getting-started/index.md
@@ -94,3 +94,15 @@ your LibreTime server has made to this Icecast server. If you have only just
installed LibreTime, there may not be any media playing out yet.

+
+Reverse Proxy Connections
+-------------------------
+In some deployments, the LibreTime server is deployed behind a reverse proxy,
+for example in containerization use-cases such as Docker and LXC. LibreTime
+makes extensive use of its API for some site functionality, which causes
+[Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
+to occur. By default, CORS requests are blocked by your browser and the origins
+need to be added to the **Allowed CORS URLs** block in
+[**General Settings**](/manual/general/). These origins should include any
+domains that will be used externally to connect to your reverse proxy that you
+want handled by LibreTime.
diff --git a/install b/install
index 1dcd695b5..ea569601c 100755
--- a/install
+++ b/install
@@ -633,6 +633,15 @@ case "${dist}-${code}" in
is_debian_stretch=true
;;
#End of fix
+ #Fix for Raspbian 10 (buster)
+ raspbian-10|10)
+ code="buster"
+ dist="debian"
+ is_debian_dist=true
+ is_debian_buster=true
+ ;;
+ #End of fix
+
debian-8|debian-jessie)
echo -e "ERROR: Debian Jessie is archived and does not receive any security or other updates since 2018-05-17." >&2
echo -e "The LibreTime installer dropped support for installing LibreTime on Jessie in 3.0.0-alpha.8." >&2
@@ -891,8 +900,19 @@ if [ "$icecast" = "t" ]; then
icecast_unit_name="icecast2"
if [ "$dist" != "centos" ]; then
sed -i 's/ENABLE=false/ENABLE=true/g' /etc/default/icecast2
+ icecast_config="/etc/icecast2/icecast.xml"
else
icecast_unit_name="icecast"
+ icecast_config="/etc/icecast.xml"
+ fi
+ # only update icecast password if
+ if [ ! -f "/etc/airtime/airtime.conf" ] && [ !-f "/etc/airtime/airtime.conf.tmp" ]; then
+ icecast_pass=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-12};)
+ echo $icecast_pass > /tmp/icecast_pass
+ loud "\n New install detected setting icecast password to random value."
+ xmlstarlet ed --inplace -u /icecast/authentication/source-password -v $icecast_pass $icecast_config
+ xmlstarlet ed --inplace -u /icecast/authentication/relay-password -v $icecast_pass $icecast_config
+ xmlstarlet ed --inplace -u /icecast/authentication/admin-password -v $icecast_pass $icecast_config
fi
# restart in case icecast was already started (like is the case on debian)
systemInitCommand restart ${icecast_unit_name}
@@ -905,6 +925,7 @@ loud "-----------------------------------------------------"
verbose "\n * Installing necessary python services..."
loudCmd "pip install setuptools --upgrade"
+loudCmd "pip install zipp==1.0.0"
verbose "...Done"
# Ubuntu Trusty and Debian Wheezy needs a workaround for python version SSL downloads
@@ -1100,6 +1121,10 @@ if [ ! -d "/etc/airtime" ]; then
mkdir /etc/airtime
fi
+if [ ! -f "/etc/airtime/airtime.conf" ] && [ !-f "/etc/airtime/airtime.conf.tmp" ]; then
+ # need to copy the icecast_pass from temp to /etc/airtime so web-based installer can read it
+ cp /tmp/icecast_pass /etc/airtime/icecast_pass
+fi
chown -R ${web_user}:${web_user} /etc/airtime
diff --git a/installer/lib/requirements-debian-buster.apt b/installer/lib/requirements-debian-buster.apt
index e50bd7aed..aceab8a25 100644
--- a/installer/lib/requirements-debian-buster.apt
+++ b/installer/lib/requirements-debian-buster.apt
@@ -59,7 +59,7 @@ libfaad2
php-apcu
lame
-
+silan
coreutils
liquidsoap
@@ -67,3 +67,5 @@ liquidsoap
libopus0
systemd-sysv
+
+xmlstarlet
diff --git a/installer/lib/requirements-debian-jessie.apt b/installer/lib/requirements-debian-jessie.apt
index 548835c90..4e22102cc 100644
--- a/installer/lib/requirements-debian-jessie.apt
+++ b/installer/lib/requirements-debian-jessie.apt
@@ -63,3 +63,5 @@ libopus0
sysvinit
sysvinit-utils
+
+xmlstarlet
diff --git a/installer/lib/requirements-debian-stretch.apt b/installer/lib/requirements-debian-stretch.apt
index 5f11226cb..c71175f6a 100644
--- a/installer/lib/requirements-debian-stretch.apt
+++ b/installer/lib/requirements-debian-stretch.apt
@@ -67,3 +67,5 @@ liquidsoap
libopus0
systemd-sysv
+
+xmlstarlet
diff --git a/installer/lib/requirements-ubuntu-bionic.apt b/installer/lib/requirements-ubuntu-bionic.apt
index 08e6f21a6..58be7819f 100644
--- a/installer/lib/requirements-ubuntu-bionic.apt
+++ b/installer/lib/requirements-ubuntu-bionic.apt
@@ -81,3 +81,5 @@ build-essential
libssl-dev
libffi-dev
python-dev
+
+xmlstarlet
diff --git a/installer/lib/requirements-ubuntu-precise.apt b/installer/lib/requirements-ubuntu-precise.apt
index 7c217f659..ed31b628f 100644
--- a/installer/lib/requirements-ubuntu-precise.apt
+++ b/installer/lib/requirements-ubuntu-precise.apt
@@ -70,3 +70,5 @@ liquidsoap-plugin-pulseaudio
liquidsoap-plugin-taglib
liquidsoap-plugin-voaacenc
liquidsoap-plugin-vorbis
+
+xmlstarlet
diff --git a/installer/lib/requirements-ubuntu-xenial.apt b/installer/lib/requirements-ubuntu-xenial.apt
index 70336c10c..41381915f 100644
--- a/installer/lib/requirements-ubuntu-xenial.apt
+++ b/installer/lib/requirements-ubuntu-xenial.apt
@@ -81,3 +81,5 @@ build-essential
libssl-dev
libffi-dev
python-dev
+
+xmlstarlet
diff --git a/installer/lib/requirements-ubuntu-xenial_docker_minimal.apt b/installer/lib/requirements-ubuntu-xenial_docker_minimal.apt
index 6d414ba2d..09c94f817 100644
--- a/installer/lib/requirements-ubuntu-xenial_docker_minimal.apt
+++ b/installer/lib/requirements-ubuntu-xenial_docker_minimal.apt
@@ -76,3 +76,5 @@ build-essential
libssl-dev
libffi-dev
python-dev
+
+xmlstarlet
diff --git a/installer/vagrant/centos.sh b/installer/vagrant/centos.sh
index ddfcecebd..ed3b0bf70 100644
--- a/installer/vagrant/centos.sh
+++ b/installer/vagrant/centos.sh
@@ -86,7 +86,8 @@ yum install -y \
policycoreutils-python \
python-celery \
python2-pika \
- lsof
+ lsof \
+ xmlstarlet
# for pip ssl install
yum install -y \
diff --git a/mkdocs.yml b/mkdocs.yml
index d744f54a4..55c686d48 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -20,6 +20,10 @@ pages:
- 'Features': features.md
- 'F.A.Q.': faq.md
- 'Rights and Royalties': manual/rights-and-royalties/index.md
+ - 'Installation':
+ - 'Install': install.md
+ - 'Preparing the Server': manual/preparing-the-server/index.md
+ - 'Setting the Server Time': manual/setting-the-server-time/index.md
- 'Using LibreTime':
- 'On Air in 60 seconds!': 'manual/on-air-in-60-seconds/index.md'
- 'Getting Started': manual/getting-started/index.md
@@ -55,10 +59,6 @@ pages:
- 'Smartphone Journalism': manual/smartphone-journalism/index.md
- 'Icecast and SHOUTcast': manual/icecast-and-shoutcast/index.md
- 'Recording Shows': manual/recording-shows/index.md
- - 'Installation':
- - 'Install': install.md
- - 'Preparing the Server': manual/preparing-the-server/index.md
- - 'Setting the Server Time': manual/setting-the-server-time/index.md
- 'Administration':
- 'Backing Up the Server': manual/backing-up-the-server/index.md
- 'Media Folders': manual/media-folders/index.md
diff --git a/python_apps/airtime_analyzer/setup.py b/python_apps/airtime_analyzer/setup.py
index e59b34593..eb697d941 100644
--- a/python_apps/airtime_analyzer/setup.py
+++ b/python_apps/airtime_analyzer/setup.py
@@ -28,7 +28,7 @@ setup(name='airtime_analyzer',
packages=['airtime_analyzer'],
scripts=['bin/airtime_analyzer'],
install_requires=[
- 'mutagen>=1.41.1', # got rid of specific version requirement
+ 'mutagen~=1.43.0', # got rid of specific version requirement
'pika',
'daemon',
'file-magic',
diff --git a/python_apps/pypo/liquidsoap/__main__.py b/python_apps/pypo/liquidsoap/__main__.py
index 54463b64c..09e4abe5f 100644
--- a/python_apps/pypo/liquidsoap/__main__.py
+++ b/python_apps/pypo/liquidsoap/__main__.py
@@ -5,6 +5,7 @@ import argparse
import os
import generate_liquidsoap_cfg
import logging
+import subprocess
PYPO_HOME = '/var/tmp/airtime/pypo/'
@@ -21,11 +22,15 @@ def run():
logging.basicConfig(level=getattr(logging, 'DEBUG', None))
generate_liquidsoap_cfg.run()
- script_path = os.path.join(os.path.dirname(__file__), 'ls_script.liq')
-
+ ''' check liquidsoap version if less than 1.3 use legacy liquidsoap script '''
+ liquidsoap_version=subprocess.check_output("liquidsoap --version", shell=True)
+ if "1.1.1" not in liquidsoap_version:
+ script_path = os.path.join(os.path.dirname(__file__), 'ls_script.liq')
+ else:
+ script_path = os.path.join(os.path.dirname(__file__), 'ls_script_legacy.liq')
if args.debug:
os.execl('/usr/bin/liquidsoap', 'airtime-liquidsoap', script_path, '--verbose', '-f', '--debug')
else:
os.execl('/usr/bin/liquidsoap', 'airtime-liquidsoap', script_path, '--verbose', '-f')
-run()
\ No newline at end of file
+run()
diff --git a/python_apps/pypo/liquidsoap/liquidsoap_auth.py b/python_apps/pypo/liquidsoap/liquidsoap_auth.py
index 838898afb..fe4725305 100644
--- a/python_apps/pypo/liquidsoap/liquidsoap_auth.py
+++ b/python_apps/pypo/liquidsoap/liquidsoap_auth.py
@@ -15,7 +15,9 @@ elif dj_type == '--dj':
response = api_clients.check_live_stream_auth(username, password, source_type)
-if 'msg' in response:
+if 'msg' in response and response['msg'] == True:
print response['msg']
+ sys.exit(0)
else:
print False
+ sys.exit(1)
diff --git a/python_apps/pypo/liquidsoap/ls_lib.liq b/python_apps/pypo/liquidsoap/ls_lib.liq
index b2822d596..1ed7495ea 100644
--- a/python_apps/pypo/liquidsoap/ls_lib.liq
+++ b/python_apps/pypo/liquidsoap/ls_lib.liq
@@ -266,7 +266,7 @@ def input.http_restart(~id,~initial_url="http://dummy/url")
source = audio_to_stereo(input.http(buffer=5.,max=15.,id=id,autostart=false,initial_url))
def stopped()
- "stopped" == list.hd(server.execute("#{id}.status"))
+ "stopped" == list.hd(server.execute("#{id}.status"), default="")
end
server.register(namespace=id,
@@ -321,7 +321,7 @@ def cross_http(~debug=true,~http_input_id,source)
cross_d = 3.
def crosser(a,b)
- url = list.hd(server.execute('#{id}.url'))
+ url = list.hd(server.execute('#{id}.url'), default="")
status = list.hd(server.execute('#{id}.status'))
on_m([("source_url",url)])
if debug then
@@ -374,7 +374,7 @@ def http_fallback(~http_input_id,~http,~default)
end
def connected()
- status = list.hd(server.execute("#{id}.status"))
+ status = list.hd(server.execute("#{id}.status"), default="")
not(list.mem(status,["polling","stopped"]))
end
connected = gracetime(connected)
diff --git a/python_apps/pypo/liquidsoap/ls_lib_legacy.liq b/python_apps/pypo/liquidsoap/ls_lib_legacy.liq
new file mode 100644
index 000000000..b2822d596
--- /dev/null
+++ b/python_apps/pypo/liquidsoap/ls_lib_legacy.liq
@@ -0,0 +1,399 @@
+def notify(m)
+ command = "timeout --signal=KILL 45 pyponotify --media-id=#{m['schedule_table_id']} &"
+ log(command)
+ system(command)
+end
+
+def notify_queue(m)
+ f = !dynamic_metadata_callback
+ ignore(f(m))
+ notify(m)
+end
+
+def notify_stream(m)
+ json_str = string.replace(pattern="\n",(fun (s) -> ""), json_of(m))
+ #if a string has a single apostrophe in it, let's comment it out by ending the string before right before it
+ #escaping the apostrophe, and then starting a new string right after it. This is why we use 3 apostrophes.
+ json_str = string.replace(pattern="'",(fun (s) -> "'\''"), json_str)
+ command = "timeout --signal=KILL 45 pyponotify --webstream='#{json_str}' --media-id=#{!current_dyn_id} &"
+
+ if !current_dyn_id != "-1" then
+ log(command)
+ system(command)
+ end
+end
+
+# A function applied to each metadata chunk
+def append_title(m) =
+ log("Using stream_format #{!stream_metadata_type}")
+
+ if list.mem_assoc("mapped", m) then
+ #protection against applying this function twice. It shouldn't be happening
+ #and bug file with Liquidsoap.
+ m
+ else
+ if !stream_metadata_type == 1 then
+ [("title", "#{!show_name} - #{m['artist']} - #{m['title']}"), ("mapped", "true")]
+ elsif !stream_metadata_type == 2 then
+ [("title", "#{!station_name} - #{!show_name}"), ("mapped", "true")]
+ else
+ if "#{m['artist']}" == "" then
+ [("title", "#{m['title']}"), ("mapped", "true")]
+ else
+ [("title", "#{m['artist']} - #{m['title']}"), ("mapped", "true")]
+ end
+ end
+ end
+end
+
+def crossfade_airtime(s)
+ #duration is automatically overwritten by metadata fields passed in
+ #with audio
+ s = fade.in(type="log", duration=0., s)
+ s = fade.out(type="log", duration=0., s)
+ fader = fun (a,b) -> add(normalize=false,[b,a])
+ cross(fader,s)
+end
+
+def transition(a,b) =
+ log("transition called...")
+ add(normalize=false,
+ [ sequence([ blank(duration=0.01),
+ fade.initial(duration=!default_dj_fade, b) ]),
+ fade.final(duration=!default_dj_fade, a) ])
+end
+
+# we need this function for special transition case(from default to queue)
+# we don't want the trasition fade to have effect on the first song that would
+# be played siwtching out of the default(silent) source
+def transition_default(a,b) =
+ log("transition called...")
+ if !just_switched then
+ just_switched := false
+ add(normalize=false,
+ [ sequence([ blank(duration=0.01),
+ fade.initial(duration=!default_dj_fade, b) ]),
+ fade.final(duration=!default_dj_fade, a) ])
+ else
+ just_switched := false
+ b
+ end
+end
+
+
+# Define a transition that fades out the
+# old source, adds a single, and then
+# plays the new source
+def to_live(old,new) =
+ # Fade out old source
+ old = fade.final(old)
+ # Compose this in sequence with
+ # the new source
+ sequence([old,new])
+end
+
+
+def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, description, genre, user, s, stream, connected, name, channels) =
+ source = ref s
+ def on_error(msg)
+ connected := "false"
+ command = "timeout --signal=KILL 45 pyponotify --error='#{msg}' --stream-id=#{stream} --time=#{!time} &"
+ system(command)
+ log(command)
+ 5.
+ end
+ def on_connect()
+ connected := "true"
+ command = "timeout --signal=KILL 45 pyponotify --connect --stream-id=#{stream} --time=#{!time} &"
+ system(command)
+ log(command)
+ end
+
+ stereo = (channels == "stereo")
+
+ if output_type == "icecast" then
+ user_ref = ref user
+ if user == "" then
+ user_ref := "source"
+ end
+ output_mono = output.icecast(host = host,
+ port = port,
+ password = pass,
+ mount = mount_point,
+ fallible = true,
+ url = url,
+ description = description,
+ name = name,
+ genre = genre,
+ user = !user_ref,
+ on_error = on_error,
+ on_connect = on_connect)
+
+ output_stereo = output.icecast(host = host,
+ port = port,
+ password = pass,
+ mount = mount_point,
+ fallible = true,
+ url = url,
+ description = description,
+ name = name,
+ genre = genre,
+ user = !user_ref,
+ on_error = on_error,
+ on_connect = on_connect)
+ if type == "mp3" then
+ %include "mp3.liq"
+ end
+ if type == "ogg" then
+ %include "ogg.liq"
+ end
+
+ %ifencoder %opus
+ if type == "opus" then
+ %include "opus.liq"
+ end
+ %endif
+
+ %ifencoder %fdkaac
+ if type == "aac" then
+ %include "fdkaac.liq"
+ end
+ %endif
+ else
+ user_ref = ref user
+ if user == "" then
+ user_ref := "source"
+ end
+
+ output_mono = output.shoutcast(id = "shoutcast_stream_#{stream}",
+ host = host,
+ port = port,
+ password = pass,
+ fallible = true,
+ url = url,
+ genre = genre,
+ name = description,
+ user = !user_ref,
+ on_error = on_error,
+ on_connect = on_connect)
+
+ output_stereo = output.shoutcast(id = "shoutcast_stream_#{stream}",
+ host = host,
+ port = port,
+ password = pass,
+ fallible = true,
+ url = url,
+ genre = genre,
+ name = description,
+ user = !user_ref,
+ on_error = on_error,
+ on_connect = on_connect)
+
+ if type == "mp3" then
+ %include "mp3.liq"
+ end
+
+ %ifencoder %fdkaac
+ if type == "aac" then
+ %include "fdkaac.liq"
+ end
+ %endif
+ end
+end
+
+# Add a skip function to a source
+# when it does not have one
+# by default
+#def add_skip_command(s)
+# # A command to skip
+# def skip(_)
+# # get playing (active) queue and flush it
+# l = list.hd(server.execute("queue.secondary_queue"))
+# l = string.split(separator=" ",l)
+# list.iter(fun (rid) -> ignore(server.execute("queue.remove #{rid}")), l)
+#
+# l = list.hd(server.execute("queue.primary_queue"))
+# l = string.split(separator=" ", l)
+# if list.length(l) > 0 then
+# source.skip(s)
+# "Skipped"
+# else
+# "Not skipped"
+# end
+# end
+# # Register the command:
+# server.register(namespace="source",
+# usage="skip",
+# description="Skip the current song.",
+# "skip",fun(s) -> begin log("source.skip") skip(s) end)
+#end
+
+def clear_queue(s)
+ source.skip(s)
+end
+
+def set_dynamic_source_id(id) =
+ current_dyn_id := id
+ string_of(!current_dyn_id)
+end
+
+def get_dynamic_source_id() =
+ string_of(!current_dyn_id)
+end
+
+#cc-4633
+
+
+# NOTE
+# A few values are hardcoded and may be dependent:
+# - the delay in gracetime is linked with the buffer duration of input.http
+# (delay should be a bit less than buffer)
+# - crossing duration should be less than buffer length
+# (at best, a higher duration will be ineffective)
+
+# HTTP input with "restart" command that waits for "stop" to be effected
+# before "start" command is issued. Optionally it takes a new URL to play,
+# which makes it a convenient replacement for "url".
+# In the future, this may become a core feature of the HTTP input.
+# TODO If we stop and restart quickly several times in a row,
+# the data bursts accumulate and create buffer overflow.
+# Flushing the buffer on restart could be a good idea, but
+# it would also create an interruptions while the buffer is
+# refilling... on the other hand, this would avoid having to
+# fade using both cross() and switch().
+def input.http_restart(~id,~initial_url="http://dummy/url")
+
+ source = audio_to_stereo(input.http(buffer=5.,max=15.,id=id,autostart=false,initial_url))
+
+ def stopped()
+ "stopped" == list.hd(server.execute("#{id}.status"))
+ end
+
+ server.register(namespace=id,
+ "restart",
+ usage="restart [url]",
+ fun (url) -> begin
+ if url != "" then
+ log(string_of(server.execute("#{id}.url #{url}")))
+ end
+ log(string_of(server.execute("#{id}.stop")))
+ add_timeout(0.5,
+ { if stopped() then
+ log(string_of(server.execute("#{id}.start"))) ;
+ (-1.)
+ else 0.5 end})
+ "OK"
+ end)
+
+ # Dummy output should be useless if HTTP stream is meant
+ # to be listened to immediately. Otherwise, apply it.
+ #
+ # output.dummy(fallible=true,source)
+
+ source
+
+end
+
+# Transitions between URL changes in HTTP streams.
+def cross_http(~debug=true,~http_input_id,source)
+
+ id = http_input_id
+ last_url = ref ""
+ change = ref false
+
+ def on_m(m)
+ notify_stream(m)
+ changed = m["source_url"] != !last_url
+ log("URL now #{m['source_url']} (change: #{changed})")
+ if changed then
+ if !last_url != "" then change := true end
+ last_url := m["source_url"]
+ end
+ end
+
+ # We use both metadata and status to know about the current URL.
+ # Using only metadata may be more precise is crazy corner cases,
+ # but it's also asking too much: the metadata may not pass through
+ # before the crosser is instantiated.
+ # Using only status in crosser misses some info, eg. on first URL.
+ source = on_metadata(on_m,source)
+
+ cross_d = 3.
+
+ def crosser(a,b)
+ url = list.hd(server.execute('#{id}.url'))
+ status = list.hd(server.execute('#{id}.status'))
+ on_m([("source_url",url)])
+ if debug then
+ log("New track inside HTTP stream")
+ log(" status: #{status}")
+ log(" need to cross: #{!change}")
+ log(" remaining #{source.remaining(a)} sec before, \
+ #{source.remaining(b)} sec after")
+ end
+ if !change then
+ change := false
+ # In principle one should avoid crossing on a live stream
+ # it'd be okay to do it here (eg. use add instead of sequence)
+ # because it's only once per URL, but be cautious.
+ sequence([fade.out(duration=cross_d,a),fade.in(b)])
+ else
+ # This is done on tracks inside a single stream.
+ # Do NOT cross here or you'll gradually empty the buffer!
+ sequence([a,b])
+ end
+ end
+
+ # Setting conservative=true would mess with the delayed switch below
+ cross(duration=cross_d,conservative=false,crosser,source)
+
+end
+
+# Custom fallback between http and default source with fading of
+# beginning and end of HTTP stream.
+# It does not take potential URL changes into account, as long as
+# they do not interrupt streaming (thanks to the HTTP buffer).
+def http_fallback(~http_input_id,~http,~default)
+
+ id = http_input_id
+
+ # We use a custom switching predicate to trigger switching (and thus,
+ # transitions) before the end of a track (rather, end of HTTP stream).
+ # It is complexified because we don't want to trigger switching when
+ # HTTP disconnects for just an instant, when changing URL: for that
+ # we use gracetime below.
+
+ def gracetime(~delay=3.,f)
+ last_true = ref 0.
+ { if f() then
+ last_true := gettimeofday()
+ true
+ else
+ gettimeofday() < !last_true+delay
+ end }
+ end
+
+ def connected()
+ status = list.hd(server.execute("#{id}.status"))
+ not(list.mem(status,["polling","stopped"]))
+ end
+ connected = gracetime(connected)
+
+ def to_live(a,b) =
+ log("TRANSITION to live")
+ add(normalize=false,
+ [fade.initial(b),fade.final(a)])
+ end
+ def to_static(a,b) =
+ log("TRANSITION to static")
+ sequence([fade.out(a),fade.initial(b)])
+ end
+
+ switch(
+ track_sensitive=false,
+ transitions=[to_live,to_static],
+ [(# make sure it is connected, and not buffering
+ {connected() and source.is_ready(http) and !webstream_enabled}, http),
+ ({true},default)])
+
+end
diff --git a/python_apps/pypo/liquidsoap/ls_script.liq b/python_apps/pypo/liquidsoap/ls_script.liq
index 14fd0f323..a832a0e1f 100644
--- a/python_apps/pypo/liquidsoap/ls_script.liq
+++ b/python_apps/pypo/liquidsoap/ls_script.liq
@@ -41,7 +41,7 @@ source_id = ref 0
def check_version(~version=liquidsoap.version, major, minor) =
v = list.map(int_of_string, string.split(separator="\.", version))
- list.nth(v,0) > major or list.nth(v,0) == major and list.nth(v,1) >= minor
+ list.nth(v,0,default=0) > major or list.nth(v,0,default=0) == major and list.nth(v,1,default=0) >= minor
end
# cue cut fix for liquidsoap <1.2.2
@@ -235,26 +235,40 @@ def master_dj_disconnect() =
update_source_status("master_dj", false)
end
-#auth function for live stream
-def check_master_dj_client(user,password) =
- log("master connected")
- #get the output of the php script
- ret = get_process_lines("python #{auth_path} --master #{user} #{password}")
- #ret has now the value of the live client (dj1,dj2, or djx), or "ERROR"/"unknown" ...
- ret = list.hd(ret)
+# Auth function for live stream
+# @Category LiveStream
+# @param user Username to check against LibreTime API
+# @param password Password to check against LibreTime API
+# @param ~type Type of password to check, "dj" or "master, default: "master"
+def check_auth(user="", password="", ~type="master") =
+ log("#{type} user #{user} connected",label="#{type}_source")
- #return true to let the client transmit data, or false to tell harbor to decline
- ret == "True"
+ # Check auth based on return value from auth script
+ ret = snd(snd(run_process("python #{auth_path} --#{type} #{user} #{password}"))) == "0"
+
+ if ret then
+ log("#{type} user #{user} authenticated",label="#{type}_source")
+ else
+ log("#{type} user #{user} auth failed",label="#{type}_source",level=2)
+ end
+
+ ret
end
-def check_dj_client(user,password) =
- log("live dj connected")
- #get the output of the php script
- ret = get_process_lines("python #{auth_path} --dj #{user} #{password}")
- #ret has now the value of the live client (dj1,dj2, or djx), or "ERROR"/"unknown" ...
- hd = list.hd(ret)
- log("Live DJ authenticated: #{hd}")
- hd == "True"
+# Check master source auth
+# @Category LiveStream
+# @param user Username to check against LibreTime API
+# @param password Password to check against LibreTime API
+def check_master_dj_client(user, password) =
+ check_auth(user, password)
+end
+
+# Check dj/show source auth
+# @Category LiveStream
+# @param user Username to check against LibreTime API
+# @param password Password to check against LibreTime API
+def check_dj_client(user, password) =
+ check_auth(user, password, type="dj")
end
s = switch(id="schedule_noise_switch",
diff --git a/python_apps/pypo/liquidsoap/ls_script_legacy.liq b/python_apps/pypo/liquidsoap/ls_script_legacy.liq
new file mode 100644
index 000000000..c4a8b99af
--- /dev/null
+++ b/python_apps/pypo/liquidsoap/ls_script_legacy.liq
@@ -0,0 +1,445 @@
+%include "/etc/airtime/liquidsoap.cfg"
+
+set("log.file.path", log_file)
+set("server.telnet", true)
+set("server.telnet.port", 1234)
+# set("init.daemon.pidfile.path", "/var/run/airtime/airtime-liquidsoap.pid")
+
+
+#Dynamic source list
+#dyn_sources = ref []
+webstream_enabled = ref false
+
+time = ref string_of(gettimeofday())
+
+#live stream setup
+set("harbor.bind_addr", "0.0.0.0")
+
+current_dyn_id = ref '-1'
+
+pypo_data = ref '0'
+stream_metadata_type = ref 0
+default_dj_fade = ref 0.
+station_name = ref ''
+show_name = ref ''
+
+dynamic_metadata_callback = ref fun (s) -> begin () end
+
+s1_connected = ref ''
+s2_connected = ref ''
+s3_connected = ref ''
+s4_connected = ref ''
+s1_namespace = ref ''
+s2_namespace = ref ''
+s3_namespace = ref ''
+just_switched = ref false
+
+%include "ls_lib_legacy.liq"
+
+sources = ref []
+source_id = ref 0
+
+def check_version(~version=liquidsoap.version, major, minor) =
+ v = list.map(int_of_string, string.split(separator="\.", version))
+ list.nth(v,0) > major or list.nth(v,0) == major and list.nth(v,1) >= minor
+end
+
+# cue cut fix for liquidsoap <1.2.2
+#
+# This was most likely broken on 1.1.1 (debian) as well.
+#
+# adapted from https://github.com/savonet/liquidsoap/issues/390#issuecomment-277562081
+#
+def fix_cue_in(~cue_in_metadata='liq_cue_in', m) =
+ # 0.04 might need to be adjusted according to your frame size
+ if float_of_string(m[cue_in_metadata]) < 0.04 then
+ [(cue_in_metadata, "0")]
+ else
+ []
+ end
+end
+
+def create_source()
+ l = request.equeue(id="s#{!source_id}", length=0.5)
+
+ l = audio_to_stereo(id="queue_src", l)
+
+ l = if not check_version(1, 3) then
+ map_metadata(fix_cue_in, l)
+ else
+ l
+ end
+ l = cue_cut(l)
+ l = amplify(1., override="replay_gain", l)
+
+ # the crossfade function controls fade in/out
+ l = crossfade_airtime(l)
+
+ l = on_metadata(notify_queue, l)
+
+ sources := list.append([l], !sources)
+ server.register(namespace="queues",
+ "s#{!source_id}_skip",
+ fun (s) -> begin log("queues.s#{!source_id}_skip")
+ clear_queue(l)
+ "Done"
+ end)
+ source_id := !source_id + 1
+end
+
+create_source()
+create_source()
+create_source()
+create_source()
+
+create_source()
+create_source()
+create_source()
+create_source()
+
+queue = add(!sources, normalize=false)
+pair = insert_metadata(queue)
+dynamic_metadata_callback := fst(pair)
+queue = snd(pair)
+
+output.dummy(fallible=true, queue)
+
+http = input.http_restart(id="http")
+http = cross_http(http_input_id="http",http)
+output.dummy(fallible=true, http)
+stream_queue = http_fallback(http_input_id="http", http=http, default=queue)
+stream_queue = map_metadata(update=false, append_title, stream_queue)
+
+ignore(output.dummy(stream_queue, fallible=true))
+
+server.register(namespace="vars",
+ "pypo_data",
+ fun (s) -> begin log("vars.pypo_data") pypo_data := s "Done" end)
+server.register(namespace="vars",
+ "stream_metadata_type",
+ fun (s) -> begin log("vars.stream_metadata_type") stream_metadata_type := int_of_string(s) s end)
+server.register(namespace="vars",
+ "show_name",
+ fun (s) -> begin log("vars.show_name") show_name := s s end)
+server.register(namespace="vars",
+ "station_name",
+ fun (s) -> begin log("vars.station_name") station_name := s s end)
+server.register(namespace="vars",
+ "bootup_time",
+ fun (s) -> begin log("vars.bootup_time") time := s s end)
+server.register(namespace="streams",
+ "connection_status",
+ fun (s) -> begin log("streams.connection_status") "1:#{!s1_connected},2:#{!s2_connected},3:#{!s3_connected},4:#{!s4_connected}" end)
+server.register(namespace="vars",
+ "default_dj_fade",
+ fun (s) -> begin log("vars.default_dj_fade") default_dj_fade := float_of_string(s) s end)
+
+server.register(namespace="dynamic_source",
+ description="Enable webstream output",
+ usage='start',
+ "output_start",
+ fun (s) -> begin log("dynamic_source.output_start")
+ notify([("schedule_table_id", !current_dyn_id)])
+ webstream_enabled := true "enabled" end)
+server.register(namespace="dynamic_source",
+ description="Enable webstream output",
+ usage='stop',
+ "output_stop",
+ fun (s) -> begin log("dynamic_source.output_stop") webstream_enabled := false "disabled" end)
+
+server.register(namespace="dynamic_source",
+ description="Set the streams cc_schedule row id",
+ usage="id ",
+ "id",
+ fun (s) -> begin log("dynamic_source.id") set_dynamic_source_id(s) end)
+
+server.register(namespace="dynamic_source",
+ description="Get the streams cc_schedule row id",
+ usage="get_id",
+ "get_id",
+ fun (s) -> begin log("dynamic_source.get_id") get_dynamic_source_id() end)
+
+#server.register(namespace="dynamic_source",
+# description="Start a new dynamic source.",
+# usage="start ",
+# "read_start",
+# fun (uri) -> begin log("dynamic_source.read_start") begin_stream_read(uri) end)
+#server.register(namespace="dynamic_source",
+# description="Stop a dynamic source.",
+# usage="stop ",
+# "read_stop",
+# fun (s) -> begin log("dynamic_source.read_stop") stop_stream_read(s) end)
+
+#server.register(namespace="dynamic_source",
+# description="Stop a dynamic source.",
+# usage="stop ",
+# "read_stop_all",
+# fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end)
+
+default = amplify(id="silence_src", 0.00001, noise())
+ref_off_air_meta = ref off_air_meta
+if !ref_off_air_meta == "" then
+ ref_off_air_meta := "LibreTime - offline"
+end
+default = rewrite_metadata([("title", !ref_off_air_meta)], default)
+ignore(output.dummy(default, fallible=true))
+
+master_dj_enabled = ref false
+live_dj_enabled = ref false
+scheduled_play_enabled = ref false
+
+def make_master_dj_available()
+ master_dj_enabled := true
+end
+
+def make_master_dj_unavailable()
+ master_dj_enabled := false
+end
+
+def make_live_dj_available()
+ live_dj_enabled := true
+end
+
+def make_live_dj_unavailable()
+ live_dj_enabled := false
+end
+
+def make_scheduled_play_available()
+ scheduled_play_enabled := true
+ just_switched := true
+end
+
+def make_scheduled_play_unavailable()
+ scheduled_play_enabled := false
+end
+
+def update_source_status(sourcename, status) =
+ command = "timeout --signal=KILL 45 pyponotify --source-name=#{sourcename} --source-status=#{status} &"
+ system(command)
+ log(command)
+end
+
+def live_dj_connect(header) =
+ update_source_status("live_dj", true)
+end
+
+def live_dj_disconnect() =
+ update_source_status("live_dj", false)
+end
+
+def master_dj_connect(header) =
+ update_source_status("master_dj", true)
+end
+
+def master_dj_disconnect() =
+ update_source_status("master_dj", false)
+end
+
+#auth function for live stream
+def check_master_dj_client(user,password) =
+ log("master connected")
+ #get the output of the php script
+ ret = get_process_lines("python #{auth_path} --master #{user} #{password}")
+ #ret has now the value of the live client (dj1,dj2, or djx), or "ERROR"/"unknown" ...
+ ret = list.hd(ret)
+
+ #return true to let the client transmit data, or false to tell harbor to decline
+ ret == "True"
+end
+
+def check_dj_client(user,password) =
+ log("live dj connected")
+ #get the output of the php script
+ ret = get_process_lines("python #{auth_path} --dj #{user} #{password}")
+ #ret has now the value of the live client (dj1,dj2, or djx), or "ERROR"/"unknown" ...
+ hd = list.hd(ret)
+ log("Live DJ authenticated: #{hd}")
+ hd == "True"
+end
+
+s = switch(id="schedule_noise_switch",
+ track_sensitive=false,
+ transitions=[transition_default, transition],
+ [({!scheduled_play_enabled}, stream_queue), ({true}, default)]
+ )
+
+s = if dj_live_stream_port != 0 and dj_live_stream_mp != "" then
+ dj_live =
+ audio_to_stereo(
+ input.harbor(id="live_dj_harbor",
+ dj_live_stream_mp,
+ port=dj_live_stream_port,
+ auth=check_dj_client,
+ max=40.,
+ on_connect=live_dj_connect,
+ on_disconnect=live_dj_disconnect))
+
+ ignore(output.dummy(dj_live, fallible=true))
+
+ switch(id="show_schedule_noise_switch",
+ track_sensitive=false,
+ transitions=[transition, transition],
+ [({!live_dj_enabled}, dj_live), ({true}, s)]
+ )
+else
+ s
+end
+
+s = if master_live_stream_port != 0 and master_live_stream_mp != "" then
+ master_dj =
+ audio_to_stereo(
+ input.harbor(id="master_harbor",
+ master_live_stream_mp,
+ port=master_live_stream_port,
+ auth=check_master_dj_client,
+ max=40.,
+ on_connect=master_dj_connect,
+ on_disconnect=master_dj_disconnect))
+
+ ignore(output.dummy(master_dj, fallible=true))
+
+ switch(id="master_show_schedule_noise_switch",
+ track_sensitive=false,
+ transitions=[transition, transition],
+ [({!master_dj_enabled}, master_dj), ({true}, s)]
+ )
+else
+ s
+end
+
+
+# Attach a skip command to the source s:
+#add_skip_command(s)
+
+server.register(namespace="streams",
+ description="Stop Master DJ source.",
+ usage="master_dj_stop",
+ "master_dj_stop",
+ fun (s) -> begin log("streams.master_dj_stop") make_master_dj_unavailable() "Done." end)
+server.register(namespace="streams",
+ description="Start Master DJ source.",
+ usage="master_dj_start",
+ "master_dj_start",
+ fun (s) -> begin log("streams.master_dj_start") make_master_dj_available() "Done." end)
+server.register(namespace="streams",
+ description="Stop Live DJ source.",
+ usage="live_dj_stop",
+ "live_dj_stop",
+ fun (s) -> begin log("streams.live_dj_stop") make_live_dj_unavailable() "Done." end)
+server.register(namespace="streams",
+ description="Start Live DJ source.",
+ usage="live_dj_start",
+ "live_dj_start",
+ fun (s) -> begin log("streams.live_dj_start") make_live_dj_available() "Done." end)
+server.register(namespace="streams",
+ description="Stop Scheduled Play source.",
+ usage="scheduled_play_stop",
+ "scheduled_play_stop",
+ fun (s) -> begin log("streams.scheduled_play_stop") make_scheduled_play_unavailable() "Done." end)
+server.register(namespace="streams",
+ description="Start Scheduled Play source.",
+ usage="scheduled_play_start",
+ "scheduled_play_start",
+ fun (s) -> begin log("streams.scheduled_play_start") make_scheduled_play_available() "Done." end)
+
+if output_sound_device then
+ success = ref false
+
+ log(output_sound_device_type)
+
+ %ifdef output.alsa
+ if output_sound_device_type == "ALSA" then
+ ignore(output.alsa(s))
+ success := true
+ end
+ %endif
+
+ %ifdef output.ao
+ if output_sound_device_type == "AO" then
+ ignore(output.ao(s))
+ success := true
+ end
+ %endif
+
+ %ifdef output.oss
+ if output_sound_device_type == "OSS" then
+ ignore(output.oss(s))
+ success := true
+ end
+ %endif
+
+ %ifdef output.portaudio
+ if output_sound_device_type == "Portaudio" then
+ ignore(output.portaudio(s))
+ success := true
+ end
+ %endif
+
+ %ifdef output.pulseaudio
+ if output_sound_device_type == "Pulseaudio" then
+ ignore(output.pulseaudio(s))
+ success := true
+ end
+ %endif
+
+ if (!success == false) then
+ ignore(output.prefered(s))
+ end
+
+end
+
+if s1_enable == true then
+ if s1_output == 'shoutcast' then
+ s1_namespace := "shoutcast_stream_1"
+ else
+ s1_namespace := s1_mount
+ end
+ server.register(namespace=!s1_namespace, "connected", fun (s) -> begin log("#{!s1_namespace}.connected") !s1_connected end)
+ output_to(s1_output, s1_type, s1_bitrate, s1_host, s1_port, s1_pass,
+ s1_mount, s1_url, s1_description, s1_genre, s1_user, s, "1",
+ s1_connected, s1_name, s1_channels)
+end
+
+if s2_enable == true then
+ if s2_output == 'shoutcast' then
+ s2_namespace := "shoutcast_stream_2"
+ else
+ s2_namespace := s2_mount
+ end
+ server.register(namespace=!s2_namespace, "connected", fun (s) -> begin log("#{!s2_namespace}.connected") !s2_connected end)
+ output_to(s2_output, s2_type, s2_bitrate, s2_host, s2_port, s2_pass,
+ s2_mount, s2_url, s2_description, s2_genre, s2_user, s, "2",
+ s2_connected, s2_name, s2_channels)
+
+end
+
+if s3_enable == true then
+ if s3_output == 'shoutcast' then
+ s3_namespace := "shoutcast_stream_3"
+ else
+ s3_namespace := s3_mount
+ end
+ server.register(namespace=!s3_namespace, "connected", fun (s) -> begin log("#{!s3_namespace}.connected") !s3_connected end)
+ output_to(s3_output, s3_type, s3_bitrate, s3_host, s3_port, s3_pass,
+ s3_mount, s3_url, s3_description, s3_genre, s3_user, s, "3",
+ s3_connected, s3_name, s3_channels)
+end
+
+s4_namespace = ref ''
+if s4_enable == true then
+ log("Stream 4 Enabled")
+ if s4_output == 'shoutcast' then
+ s4_namespace := "shoutcast_stream_4"
+ else
+ s4_namespace := s4_mount
+ end
+ server.register(namespace=!s4_namespace, "connected", fun (s) -> begin log("#{!s4_namespace}.connected") !s4_connected end)
+ output_to(s4_output, s4_type, s4_bitrate, s4_host, s4_port, s4_pass,
+ s4_mount, s4_url, s4_name, s4_genre, s4_user, s, "4",
+ s4_connected, s4_description, s4_channels)
+end
+
+
+command = "timeout --signal=KILL 45 pyponotify --liquidsoap-started &"
+log(command)
+system(command)