add API v2
This commit is contained in:
parent
f809c3a8ff
commit
2df0189a90
|
@ -19,4 +19,5 @@ airtime_mvc/tests/log/*.log
|
||||||
/docs/_site/*
|
/docs/_site/*
|
||||||
/docs/.jekyll-cache/*
|
/docs/.jekyll-cache/*
|
||||||
/docs/.gems/*
|
/docs/.gems/*
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
|
api.log
|
||||||
|
|
|
@ -10,6 +10,10 @@ Vagrant.configure("2") do |config|
|
||||||
# liquidsoap input harbors for instreaming (ie. /master)
|
# liquidsoap input harbors for instreaming (ie. /master)
|
||||||
config.vm.network "forwarded_port", guest: 8001, host:8001
|
config.vm.network "forwarded_port", guest: 8001, host:8001
|
||||||
config.vm.network "forwarded_port", guest: 8002, host:8002
|
config.vm.network "forwarded_port", guest: 8002, host:8002
|
||||||
|
# database
|
||||||
|
config.vm.network "forwarded_port", guest: 5432, host:5432
|
||||||
|
# api
|
||||||
|
config.vm.network "forwarded_port", guest: 8081, host:8081
|
||||||
|
|
||||||
# make sure we are using nfs (doesn't work out of the box with debian)
|
# make sure we are using nfs (doesn't work out of the box with debian)
|
||||||
nfsPath = "."
|
nfsPath = "."
|
||||||
|
|
|
@ -18,6 +18,7 @@ $pypo = $externalServices["pypo"];
|
||||||
$liquidsoap = $externalServices["liquidsoap"];
|
$liquidsoap = $externalServices["liquidsoap"];
|
||||||
$analyzer = $externalServices["analyzer"];
|
$analyzer = $externalServices["analyzer"];
|
||||||
$celery = $externalServices['celery'];
|
$celery = $externalServices['celery'];
|
||||||
|
$api = $externalServices['api'];
|
||||||
|
|
||||||
$r1 = array_reduce($phpDependencies, "booleanReduce", true);
|
$r1 = array_reduce($phpDependencies, "booleanReduce", true);
|
||||||
$r2 = array_reduce($externalServices, "booleanReduce", true);
|
$r2 = array_reduce($externalServices, "booleanReduce", true);
|
||||||
|
@ -29,14 +30,14 @@ $result = $r1 && $r2;
|
||||||
<link rel="stylesheet" type="text/css" href="css/setup/config-check.css">
|
<link rel="stylesheet" type="text/css" href="css/setup/config-check.css">
|
||||||
</head>
|
</head>
|
||||||
<style>
|
<style>
|
||||||
/*
|
/*
|
||||||
This is here because we're using the config-check css for
|
This is here because we're using the config-check css for
|
||||||
both this page and the system status page
|
both this page and the system status page
|
||||||
*/
|
*/
|
||||||
html {
|
html {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
|
@ -156,7 +157,7 @@ $result = $r1 && $r2;
|
||||||
?>">
|
?>">
|
||||||
Make sure RabbitMQ is installed correctly, and that your settings in /etc/airtime/airtime.conf
|
Make sure RabbitMQ is installed correctly, and that your settings in /etc/airtime/airtime.conf
|
||||||
are correct. Try using <code>sudo rabbitmqctl list_users</code> and <code>sudo rabbitmqctl list_vhosts</code>
|
are correct. Try using <code>sudo rabbitmqctl list_users</code> and <code>sudo rabbitmqctl list_vhosts</code>
|
||||||
to see if the airtime user (or your custom RabbitMQ user) exists, then checking that
|
to see if the airtime user (or your custom RabbitMQ user) exists, then checking that
|
||||||
<code>sudo rabbitmqctl list_exchanges</code> contains entries for airtime-pypo and airtime-uploads.
|
<code>sudo rabbitmqctl list_exchanges</code> contains entries for airtime-pypo and airtime-uploads.
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
@ -243,6 +244,26 @@ $result = $r1 && $r2;
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="<?=$api ? 'success' : 'danger';?>">
|
||||||
|
<td class="component">
|
||||||
|
API
|
||||||
|
</td>
|
||||||
|
<td class="description">
|
||||||
|
<?php echo _("LibreTime API service") ?>
|
||||||
|
</td>
|
||||||
|
<td class="solution <?php if ($api) {echo 'check';?>" >
|
||||||
|
<?php
|
||||||
|
} else {
|
||||||
|
?>">
|
||||||
|
<?php echo _("Check that the libretime-api service is installed correctly in ") ?><code>/etc/init.d/</code>,
|
||||||
|
<?php echo _(" and ensure that it's running with ") ?>
|
||||||
|
<br/><code>systemctl status libretime-api</code><br/>
|
||||||
|
<?php echo _("If not, try ") ?><br/><code>sudo systemctl restart libretime-api</code>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
$liquidsoap = $externalServices["liquidsoap"];
|
$liquidsoap = $externalServices["liquidsoap"];
|
||||||
$analyzer = $externalServices["analyzer"];
|
$analyzer = $externalServices["analyzer"];
|
||||||
$celery = $externalServices['celery'];
|
$celery = $externalServices['celery'];
|
||||||
|
$api = $externalServices['api'];
|
||||||
|
|
||||||
$r1 = array_reduce($phpDependencies, "booleanReduce", true);
|
$r1 = array_reduce($phpDependencies, "booleanReduce", true);
|
||||||
$r2 = array_reduce($externalServices, "booleanReduce", true);
|
$r2 = array_reduce($externalServices, "booleanReduce", true);
|
||||||
|
@ -170,6 +171,26 @@
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="component">
|
||||||
|
API
|
||||||
|
</td>
|
||||||
|
<td class="description">
|
||||||
|
<?php echo _("LibreTime API service") ?>
|
||||||
|
</td>
|
||||||
|
<td class="solution <?php if ($api) {echo 'check';?>" >
|
||||||
|
<?php
|
||||||
|
} else {
|
||||||
|
?>">
|
||||||
|
<?php echo _("Check that the libretime-api service is installed correctly in ") ?><code>/etc/systemd/system/</code>,
|
||||||
|
<?php echo _(" and ensure that it's running with ") ?>
|
||||||
|
<br/><code>systemctl status libretime-api</code><br/>
|
||||||
|
<?php echo _("If not, try ") ?><br/><code>sudo systemctl restart libretime-api</code>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tr id="partitions" class="even">
|
<tr id="partitions" class="even">
|
||||||
<th colspan="5"><?php echo _("Disk Space") ?></th>
|
<th colspan="5"><?php echo _("Disk Space") ?></th>
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
<p>
|
<p>
|
||||||
Looks like you're almost done! As a final step, please run the following commands from the terminal:
|
Looks like you're almost done! As a final step, please run the following commands from the terminal:
|
||||||
</p>
|
</p>
|
||||||
<pre style="text-align: left">sudo systemctl start libretime-playout
|
<pre style="text-align: left">sudo systemctl start libretime-analyzer
|
||||||
|
sudo systemctl start libretime-api
|
||||||
|
sudo systemctl start libretime-celery
|
||||||
sudo systemctl start libretime-liquidsoap
|
sudo systemctl start libretime-liquidsoap
|
||||||
sudo systemctl start libretime-analyzer
|
sudo systemctl start libretime-playout</pre
|
||||||
sudo systemctl start libretime-celery</pre>
|
|
||||||
<p>
|
<p>
|
||||||
Click "Done!" to bring up the Libretime configuration checklist; if your configuration is all green,
|
Click "Done!" to bring up the Libretime configuration checklist; if your configuration is all green,
|
||||||
you're ready to get started with your personal Libretime station!
|
you're ready to get started with your personal Libretime station!
|
||||||
|
|
|
@ -56,6 +56,7 @@ function checkExternalServices() {
|
||||||
"liquidsoap" => checkLiquidsoapService(),
|
"liquidsoap" => checkLiquidsoapService(),
|
||||||
"rabbitmq" => checkRMQConnection(),
|
"rabbitmq" => checkRMQConnection(),
|
||||||
"celery" => checkCeleryService(),
|
"celery" => checkCeleryService(),
|
||||||
|
"api" => checkApiService(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,3 +159,16 @@ function checkCeleryService() {
|
||||||
}
|
}
|
||||||
return $status == 0;
|
return $status == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if libretime-api is currently running
|
||||||
|
*
|
||||||
|
* @return boolean true if libretime-api is running
|
||||||
|
*/
|
||||||
|
function checkApiService() {
|
||||||
|
exec("pgrep -f -u www-data uwsgi", $out, $status);
|
||||||
|
if (array_key_exists(0, $out) && $status == 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return $status == 0;
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* Wrapper class for validating and installing the Airtime database during the installation process
|
* Wrapper class for validating and installing the Airtime database during the installation process
|
||||||
*/
|
*/
|
||||||
class DatabaseSetup extends Setup {
|
class DatabaseSetup extends Setup {
|
||||||
|
|
||||||
// airtime.conf section header
|
// airtime.conf section header
|
||||||
protected static $_section = "[database]";
|
protected static $_section = "[database]";
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ class DatabaseSetup extends Setup {
|
||||||
$this->checkSchemaExists();
|
$this->checkSchemaExists();
|
||||||
$this->createDatabaseTables();
|
$this->createDatabaseTables();
|
||||||
$this->updateIcecastPassword();
|
$this->updateIcecastPassword();
|
||||||
|
$this->updateDjangoTables();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -190,7 +191,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -198,7 +199,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_admin_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_admin_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -206,7 +207,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's2_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's2_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -214,7 +215,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's2_admin_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's2_admin_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -223,7 +224,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's3_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's3_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -231,7 +232,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's3_admin_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's3_admin_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -239,7 +240,7 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_admin_pass'");
|
$statement = self::$dbh->prepare("UPDATE cc_stream_setting SET value = :icecastpass WHERE keyname = 's1_admin_pass'");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
|
@ -247,11 +248,17 @@ class DatabaseSetup extends Setup {
|
||||||
$statement = self::$dbh->prepare("INSERT INTO cc_pref (keystr, valstr) VALUES ('default_icecast_password', :icecastpass )");
|
$statement = self::$dbh->prepare("INSERT INTO cc_pref (keystr, valstr) VALUES ('default_icecast_password', :icecastpass )");
|
||||||
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
$statement->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||||
try {
|
try {
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
}
|
}
|
||||||
catch (PDOException $ex) {
|
catch (PDOException $ex) {
|
||||||
print "Error!: " . $ex->getMessage() . "<br />";
|
print "Error!: " . $ex->getMessage() . "<br />";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the Django related tables for the API
|
||||||
|
*/
|
||||||
|
private function updateDjangoTables() {
|
||||||
|
shell_exec('LIBRETIME_CONF_FILE=/etc/airtime/airtime.conf.temp libretime-api migrate');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# LibreTime API
|
||||||
|
|
||||||
|
This API provides access to LibreTime's database via a Django application. This
|
||||||
|
API supersedes the [PHP API](../airtime_mvc/application/controllers/ApiController.php).
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
Deploying in a production environment is done in the [`install`](../install)
|
||||||
|
script which installs LibreTime. This is how the API is installed in the Vagrant
|
||||||
|
development images too. This method does not automatically reflect changes to
|
||||||
|
this API. After any changes, the `libretime-api` systemd service needs
|
||||||
|
restarting:
|
||||||
|
|
||||||
|
sudo systemctl restart libretime-api
|
||||||
|
|
||||||
|
Connections to the API are proxied through the Apache web server by default.
|
||||||
|
Endpoint exploration and documentation is available from
|
||||||
|
`http://example.com/api/v2/`, where `example.com` is the URL for the LibreTime
|
||||||
|
instance.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
For a live reloading version within Vagrant:
|
||||||
|
|
||||||
|
```
|
||||||
|
vagrant up debian-buster
|
||||||
|
# Run through the web setup http://localhost:8080
|
||||||
|
vagrant ssh debian-buster
|
||||||
|
sudo systemctl stop libretime-api
|
||||||
|
sudo systemctl restart libretime-analyzer libretime-celery libretime-liquidsoap libretime-playout
|
||||||
|
cd /vagrant/api
|
||||||
|
sudo pip3 install -e .
|
||||||
|
sudo -u www-data LIBRETIME_DEBUG=True python3 bin/libretime-api runserver 0.0.0.0:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
Unit tests can be run in the vagrant environment using
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo -u www-data LIBRETIME_DEBUG=True python3 bin/libretime-api test libretimeapi
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3rd Party Licences
|
||||||
|
`libretimeapi/tests/resources/song.mp3`: Steps - Tears On The Dancefloor (Album
|
||||||
|
Teaser) by mceyedol. Downloaded from
|
||||||
|
https://soundcloud.com/mceyedol/steps-tears-on-the-dancefloor-album-teaser
|
||||||
|
released under a Creative Commons Licence
|
||||||
|
([cc-by-sa-nc-sa](https://creativecommons.org/licenses/by-nc-sa/3.0/))
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'libretimeapi.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=LibreTime API Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/uwsgi /etc/airtime/libretime-api.ini
|
||||||
|
User=libretime-api
|
||||||
|
Group=libretime-api
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
|
|
||||||
|
class LibreTimeAPIConfig(AppConfig):
|
||||||
|
name = 'libretimeapi'
|
||||||
|
verbose_name = 'LibreTime API'
|
||||||
|
default_auto_field = 'django.db.models.AutoField'
|
|
@ -0,0 +1,20 @@
|
||||||
|
from django.contrib.auth.models import BaseUserManager
|
||||||
|
|
||||||
|
class UserManager(BaseUserManager):
|
||||||
|
def create_user(self, username, type, email, first_name, last_name, password):
|
||||||
|
user = self.model(username=username,
|
||||||
|
type=type,
|
||||||
|
email=email,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, username, email, first_name, last_name, password):
|
||||||
|
user = self.create_user(username, 'A', email, first_name, last_name, password)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_by_natural_key(self, username):
|
||||||
|
return self.get(username=username)
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
from .authentication import *
|
||||||
|
from .celery import *
|
||||||
|
from .countries import *
|
||||||
|
from .files import *
|
||||||
|
from .playlists import *
|
||||||
|
from .playout import *
|
||||||
|
from .podcasts import *
|
||||||
|
from .preferences import *
|
||||||
|
from .schedule import *
|
||||||
|
from .services import *
|
||||||
|
from .shows import *
|
||||||
|
from .smart_blocks import *
|
||||||
|
from .tracks import *
|
||||||
|
from .webstreams import *
|
|
@ -0,0 +1,141 @@
|
||||||
|
import hashlib
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, Permission
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.db import models
|
||||||
|
from libretimeapi.managers import UserManager
|
||||||
|
from libretimeapi.permission_constants import GROUPS
|
||||||
|
from .user_constants import USER_TYPES, ADMIN
|
||||||
|
|
||||||
|
|
||||||
|
class LoginAttempt(models.Model):
|
||||||
|
ip = models.CharField(primary_key=True, max_length=32)
|
||||||
|
attempts = models.IntegerField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_login_attempts'
|
||||||
|
|
||||||
|
|
||||||
|
class Session(models.Model):
|
||||||
|
sessid = models.CharField(primary_key=True, max_length=32)
|
||||||
|
userid = models.ForeignKey('User', models.DO_NOTHING, db_column='userid', blank=True, null=True)
|
||||||
|
login = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
ts = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_sess'
|
||||||
|
|
||||||
|
|
||||||
|
USER_TYPE_CHOICES = ()
|
||||||
|
for item in USER_TYPES.items():
|
||||||
|
USER_TYPE_CHOICES = USER_TYPE_CHOICES + (item,)
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractBaseUser):
|
||||||
|
username = models.CharField(db_column='login', unique=True, max_length=255)
|
||||||
|
password = models.CharField(db_column='pass', max_length=255) # Field renamed because it was a Python reserved word.
|
||||||
|
type = models.CharField(max_length=1, choices=USER_TYPE_CHOICES)
|
||||||
|
first_name = models.CharField(max_length=255)
|
||||||
|
last_name = models.CharField(max_length=255)
|
||||||
|
last_login = models.DateTimeField(db_column='lastlogin', blank=True, null=True)
|
||||||
|
lastfail = models.DateTimeField(blank=True, null=True)
|
||||||
|
skype_contact = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
jabber_contact = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
email = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
cell_phone = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
login_attempts = models.IntegerField(blank=True, null=True)
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'username'
|
||||||
|
EMAIL_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = ['type', 'email', 'first_name', 'last_name']
|
||||||
|
objects = UserManager()
|
||||||
|
|
||||||
|
def get_full_name(self):
|
||||||
|
return '{} {}'.format(self.first_name, self.last_name)
|
||||||
|
|
||||||
|
def get_short_name(self):
|
||||||
|
return self.first_name
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
if not password:
|
||||||
|
self.set_unusable_password()
|
||||||
|
else:
|
||||||
|
self.password = hashlib.md5(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
def is_staff(self):
|
||||||
|
print('is_staff')
|
||||||
|
return self.type == ADMIN
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
if self.has_usable_password():
|
||||||
|
test_password = hashlib.md5(password.encode()).hexdigest()
|
||||||
|
return test_password == self.password
|
||||||
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
The following methods have to be re-implemented here, as PermissionsMixin
|
||||||
|
assumes that the User class has a 'group' attribute, which LibreTime does
|
||||||
|
not currently provide. Once Django starts managing the Database
|
||||||
|
(managed = True), then this can be replaced with
|
||||||
|
django.contrib.auth.models.PermissionMixin.
|
||||||
|
"""
|
||||||
|
def is_superuser(self):
|
||||||
|
return self.type == ADMIN
|
||||||
|
|
||||||
|
def get_user_permissions(self, obj=None):
|
||||||
|
"""
|
||||||
|
Users do not have permissions directly, only through groups
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_group_permissions(self, obj=None):
|
||||||
|
permissions = GROUPS[self.type]
|
||||||
|
if obj:
|
||||||
|
obj_name = obj.__class__.__name__.lower()
|
||||||
|
permissions = [perm for perm in permissions if obj_name in perm]
|
||||||
|
# get permissions objects
|
||||||
|
q = models.Q()
|
||||||
|
for perm in permissions:
|
||||||
|
q = q | models.Q(codename=perm)
|
||||||
|
return list(Permission.objects.filter(q))
|
||||||
|
|
||||||
|
def get_all_permissions(self, obj=None):
|
||||||
|
return self.get_user_permissions(obj) + self.get_group_permissions(obj)
|
||||||
|
|
||||||
|
def has_perm(self, perm, obj=None):
|
||||||
|
if self.is_superuser():
|
||||||
|
return True
|
||||||
|
if not perm:
|
||||||
|
return False
|
||||||
|
permissions = self.get_all_permissions(obj)
|
||||||
|
try:
|
||||||
|
permission = Permission.objects.get(codename=perm)
|
||||||
|
return permission in permissions
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_perms(self, perm_list, obj=None):
|
||||||
|
result = True
|
||||||
|
for permission in perm_list:
|
||||||
|
result = result and self.has_perm(permission, obj)
|
||||||
|
return result
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_subjs'
|
||||||
|
|
||||||
|
|
||||||
|
class UserToken(models.Model):
|
||||||
|
user = models.ForeignKey(User, models.DO_NOTHING)
|
||||||
|
action = models.CharField(max_length=255)
|
||||||
|
token = models.CharField(unique=True, max_length=40)
|
||||||
|
created = models.DateTimeField()
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.user
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_subjs_token'
|
|
@ -0,0 +1,13 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryTask(models.Model):
|
||||||
|
task_id = models.CharField(max_length=256)
|
||||||
|
track_reference = models.ForeignKey('ThirdPartyTrackReference', models.DO_NOTHING, db_column='track_reference')
|
||||||
|
name = models.CharField(max_length=256, blank=True, null=True)
|
||||||
|
dispatch_time = models.DateTimeField(blank=True, null=True)
|
||||||
|
status = models.CharField(max_length=256)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'celery_tasks'
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Country(models.Model):
|
||||||
|
isocode = models.CharField(primary_key=True, max_length=3)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_country'
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class File(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
mime = models.CharField(max_length=255)
|
||||||
|
ftype = models.CharField(max_length=128)
|
||||||
|
directory = models.ForeignKey('MusicDir', models.DO_NOTHING, db_column='directory', blank=True, null=True)
|
||||||
|
filepath = models.TextField(blank=True, null=True)
|
||||||
|
import_status = models.IntegerField()
|
||||||
|
currently_accessing = models.IntegerField(db_column='currentlyaccessing')
|
||||||
|
edited_by = models.ForeignKey('User', models.DO_NOTHING, db_column='editedby', blank=True, null=True, related_name='edited_files')
|
||||||
|
mtime = models.DateTimeField(blank=True, null=True)
|
||||||
|
utime = models.DateTimeField(blank=True, null=True)
|
||||||
|
lptime = models.DateTimeField(blank=True, null=True)
|
||||||
|
md5 = models.CharField(max_length=32, blank=True, null=True)
|
||||||
|
track_title = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
artist_name = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
bit_rate = models.IntegerField(blank=True, null=True)
|
||||||
|
sample_rate = models.IntegerField(blank=True, null=True)
|
||||||
|
format = models.CharField(max_length=128, blank=True, null=True)
|
||||||
|
length = models.DurationField(blank=True, null=True)
|
||||||
|
album_title = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
genre = models.CharField(max_length=64, blank=True, null=True)
|
||||||
|
comments = models.TextField(blank=True, null=True)
|
||||||
|
year = models.CharField(max_length=16, blank=True, null=True)
|
||||||
|
track_number = models.IntegerField(blank=True, null=True)
|
||||||
|
channels = models.IntegerField(blank=True, null=True)
|
||||||
|
url = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
bpm = models.IntegerField(blank=True, null=True)
|
||||||
|
rating = models.CharField(max_length=8, blank=True, null=True)
|
||||||
|
encoded_by = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
disc_number = models.CharField(max_length=8, blank=True, null=True)
|
||||||
|
mood = models.CharField(max_length=64, blank=True, null=True)
|
||||||
|
label = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
composer = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
encoder = models.CharField(max_length=64, blank=True, null=True)
|
||||||
|
checksum = models.CharField(max_length=256, blank=True, null=True)
|
||||||
|
lyrics = models.TextField(blank=True, null=True)
|
||||||
|
orchestra = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
conductor = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
lyricist = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
original_lyricist = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
radio_station_name = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
info_url = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
artist_url = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
audio_source_url = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
radio_station_url = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
buy_this_url = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
isrc_number = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
catalog_number = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
original_artist = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
copyright = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
report_datetime = models.CharField(max_length=32, blank=True, null=True)
|
||||||
|
report_location = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
report_organization = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
subject = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
contributor = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
language = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
file_exists = models.BooleanField(blank=True, null=True)
|
||||||
|
replay_gain = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
|
||||||
|
owner = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
cuein = models.DurationField(blank=True, null=True)
|
||||||
|
cueout = models.DurationField(blank=True, null=True)
|
||||||
|
silan_check = models.BooleanField(blank=True, null=True)
|
||||||
|
hidden = models.BooleanField(blank=True, null=True)
|
||||||
|
is_scheduled = models.BooleanField(blank=True, null=True)
|
||||||
|
is_playlist = models.BooleanField(blank=True, null=True)
|
||||||
|
filesize = models.IntegerField()
|
||||||
|
description = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
artwork = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
track_type = models.CharField(max_length=16, blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.owner
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_files'
|
||||||
|
permissions = [
|
||||||
|
('change_own_file', 'Change the files where they are the owner'),
|
||||||
|
('delete_own_file', 'Delete the files where they are the owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MusicDir(models.Model):
|
||||||
|
directory = models.TextField(unique=True, blank=True, null=True)
|
||||||
|
type = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
exists = models.BooleanField(blank=True, null=True)
|
||||||
|
watched = models.BooleanField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_music_dirs'
|
||||||
|
|
||||||
|
|
||||||
|
class CloudFile(models.Model):
|
||||||
|
storage_backend = models.CharField(max_length=512)
|
||||||
|
resource_id = models.TextField()
|
||||||
|
filename = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True,
|
||||||
|
db_column='cc_file_id')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cloud_file'
|
|
@ -0,0 +1,41 @@
|
||||||
|
from django.db import models
|
||||||
|
from .files import File
|
||||||
|
from .smart_blocks import SmartBlock
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
mtime = models.DateTimeField(blank=True, null=True)
|
||||||
|
utime = models.DateTimeField(blank=True, null=True)
|
||||||
|
creator = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
description = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
length = models.DurationField(blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.creator
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_playlist'
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistContent(models.Model):
|
||||||
|
playlist = models.ForeignKey(Playlist, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
block = models.ForeignKey(SmartBlock, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
stream_id = models.IntegerField(blank=True, null=True)
|
||||||
|
type = models.SmallIntegerField()
|
||||||
|
position = models.IntegerField(blank=True, null=True)
|
||||||
|
trackoffset = models.FloatField()
|
||||||
|
cliplength = models.DurationField(blank=True, null=True)
|
||||||
|
cuein = models.DurationField(blank=True, null=True)
|
||||||
|
cueout = models.DurationField(blank=True, null=True)
|
||||||
|
fadein = models.TimeField(blank=True, null=True)
|
||||||
|
fadeout = models.TimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.playlist.owner
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_playlistcontents'
|
|
@ -0,0 +1,73 @@
|
||||||
|
from django.db import models
|
||||||
|
from .files import File
|
||||||
|
|
||||||
|
|
||||||
|
class ListenerCount(models.Model):
|
||||||
|
timestamp = models.ForeignKey('Timestamp', models.DO_NOTHING)
|
||||||
|
mount_name = models.ForeignKey('MountName', models.DO_NOTHING)
|
||||||
|
listener_count = models.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_listener_count'
|
||||||
|
|
||||||
|
|
||||||
|
class LiveLog(models.Model):
|
||||||
|
state = models.CharField(max_length=32)
|
||||||
|
start_time = models.DateTimeField()
|
||||||
|
end_time = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_live_log'
|
||||||
|
|
||||||
|
|
||||||
|
class PlayoutHistory(models.Model):
|
||||||
|
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
starts = models.DateTimeField()
|
||||||
|
ends = models.DateTimeField(blank=True, null=True)
|
||||||
|
instance = models.ForeignKey('ShowInstance', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_playout_history'
|
||||||
|
|
||||||
|
|
||||||
|
class PlayoutHistoryMetadata(models.Model):
|
||||||
|
history = models.ForeignKey(PlayoutHistory, models.DO_NOTHING)
|
||||||
|
key = models.CharField(max_length=128)
|
||||||
|
value = models.CharField(max_length=128)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_playout_history_metadata'
|
||||||
|
|
||||||
|
|
||||||
|
class PlayoutHistoryTemplate(models.Model):
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
type = models.CharField(max_length=35)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_playout_history_template'
|
||||||
|
|
||||||
|
|
||||||
|
class PlayoutHistoryTemplateField(models.Model):
|
||||||
|
template = models.ForeignKey(PlayoutHistoryTemplate, models.DO_NOTHING)
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
label = models.CharField(max_length=128)
|
||||||
|
type = models.CharField(max_length=128)
|
||||||
|
is_file_md = models.BooleanField()
|
||||||
|
position = models.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_playout_history_template_field'
|
||||||
|
|
||||||
|
|
||||||
|
class Timestamp(models.Model):
|
||||||
|
timestamp = models.DateTimeField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_timestamp'
|
|
@ -0,0 +1,77 @@
|
||||||
|
from django.db import models
|
||||||
|
from .authentication import User
|
||||||
|
from .files import File
|
||||||
|
|
||||||
|
|
||||||
|
class ImportedPodcast(models.Model):
|
||||||
|
auto_ingest = models.BooleanField()
|
||||||
|
auto_ingest_timestamp = models.DateTimeField(blank=True, null=True)
|
||||||
|
album_override = models.BooleanField()
|
||||||
|
podcast = models.ForeignKey('Podcast', models.DO_NOTHING)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.podcast.owner
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'imported_podcast'
|
||||||
|
|
||||||
|
|
||||||
|
class Podcast(models.Model):
|
||||||
|
url = models.CharField(max_length=4096)
|
||||||
|
title = models.CharField(max_length=4096)
|
||||||
|
creator = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
description = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
language = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
copyright = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
link = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
itunes_author = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
itunes_keywords = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
itunes_summary = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
itunes_subtitle = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
itunes_category = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
itunes_explicit = models.CharField(max_length=4096, blank=True, null=True)
|
||||||
|
owner = models.ForeignKey(User, models.DO_NOTHING, db_column='owner', blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.owner
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'podcast'
|
||||||
|
permissions = [
|
||||||
|
('change_own_podcast', 'Change the podcasts where they are the owner'),
|
||||||
|
('delete_own_podcast', 'Delete the podcasts where they are the owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PodcastEpisode(models.Model):
|
||||||
|
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
podcast = models.ForeignKey(Podcast, models.DO_NOTHING)
|
||||||
|
publication_date = models.DateTimeField()
|
||||||
|
download_url = models.CharField(max_length=4096)
|
||||||
|
episode_guid = models.CharField(max_length=4096)
|
||||||
|
episode_title = models.CharField(max_length=4096)
|
||||||
|
episode_description = models.TextField()
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.podcast.owner
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'podcast_episodes'
|
||||||
|
permissions = [
|
||||||
|
('change_own_podcastepisode', 'Change the episodes of podcasts where they are the owner'),
|
||||||
|
('delete_own_podcastepisode', 'Delete the episodes of podcasts where they are the owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StationPodcast(models.Model):
|
||||||
|
podcast = models.ForeignKey(Podcast, models.DO_NOTHING)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.podcast.owner
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'station_podcast'
|
|
@ -0,0 +1,30 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Preference(models.Model):
|
||||||
|
subjid = models.ForeignKey('User', models.DO_NOTHING, db_column='subjid', blank=True, null=True)
|
||||||
|
keystr = models.CharField(unique=True, max_length=255, blank=True, null=True)
|
||||||
|
valstr = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_pref'
|
||||||
|
unique_together = (('subjid', 'keystr'),)
|
||||||
|
|
||||||
|
|
||||||
|
class MountName(models.Model):
|
||||||
|
mount_name = models.CharField(max_length=1024)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_mount_name'
|
||||||
|
|
||||||
|
|
||||||
|
class StreamSetting(models.Model):
|
||||||
|
keyname = models.CharField(primary_key=True, max_length=64)
|
||||||
|
value = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
type = models.CharField(max_length=16)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_stream_setting'
|
|
@ -0,0 +1,30 @@
|
||||||
|
from django.db import models
|
||||||
|
from .files import File
|
||||||
|
|
||||||
|
|
||||||
|
class Schedule(models.Model):
|
||||||
|
starts = models.DateTimeField()
|
||||||
|
ends = models.DateTimeField()
|
||||||
|
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
stream = models.ForeignKey('Webstream', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
clip_length = models.DurationField(blank=True, null=True)
|
||||||
|
fade_in = models.TimeField(blank=True, null=True)
|
||||||
|
fade_out = models.TimeField(blank=True, null=True)
|
||||||
|
cue_in = models.DurationField()
|
||||||
|
cue_out = models.DurationField()
|
||||||
|
media_item_played = models.BooleanField(blank=True, null=True)
|
||||||
|
instance = models.ForeignKey('ShowInstance', models.DO_NOTHING)
|
||||||
|
playout_status = models.SmallIntegerField()
|
||||||
|
broadcasted = models.SmallIntegerField()
|
||||||
|
position = models.IntegerField()
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.instance.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_schedule'
|
||||||
|
permissions = [
|
||||||
|
('change_own_schedule', 'Change the content on their shows'),
|
||||||
|
('delete_own_schedule', 'Delete the content on their shows'),
|
||||||
|
]
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceRegister(models.Model):
|
||||||
|
name = models.CharField(primary_key=True, max_length=32)
|
||||||
|
ip = models.CharField(max_length=45)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_service_register'
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
from django.db import models
|
||||||
|
from .playlists import Playlist
|
||||||
|
from .files import File
|
||||||
|
|
||||||
|
|
||||||
|
class Show(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
url = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
genre = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
description = models.CharField(max_length=8192, blank=True, null=True)
|
||||||
|
color = models.CharField(max_length=6, blank=True, null=True)
|
||||||
|
background_color = models.CharField(max_length=6, blank=True, null=True)
|
||||||
|
live_stream_using_airtime_auth = models.BooleanField(blank=True, null=True)
|
||||||
|
live_stream_using_custom_auth = models.BooleanField(blank=True, null=True)
|
||||||
|
live_stream_user = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
live_stream_pass = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
linked = models.BooleanField()
|
||||||
|
is_linkable = models.BooleanField()
|
||||||
|
image_path = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
has_autoplaylist = models.BooleanField()
|
||||||
|
autoplaylist = models.ForeignKey(Playlist, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
autoplaylist_repeat = models.BooleanField()
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.showhost_set.all()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_show'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowDays(models.Model):
|
||||||
|
first_show = models.DateField()
|
||||||
|
last_show = models.DateField(blank=True, null=True)
|
||||||
|
start_time = models.TimeField()
|
||||||
|
timezone = models.CharField(max_length=1024)
|
||||||
|
duration = models.CharField(max_length=1024)
|
||||||
|
day = models.SmallIntegerField(blank=True, null=True)
|
||||||
|
repeat_type = models.SmallIntegerField()
|
||||||
|
next_pop_date = models.DateField(blank=True, null=True)
|
||||||
|
show = models.ForeignKey(Show, models.DO_NOTHING)
|
||||||
|
record = models.SmallIntegerField(blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.show.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_show_days'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowHost(models.Model):
|
||||||
|
show = models.ForeignKey(Show, models.DO_NOTHING)
|
||||||
|
subjs = models.ForeignKey('User', models.DO_NOTHING)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_show_hosts'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowInstance(models.Model):
|
||||||
|
description = models.CharField(max_length=8192, blank=True, null=True)
|
||||||
|
starts = models.DateTimeField()
|
||||||
|
ends = models.DateTimeField()
|
||||||
|
show = models.ForeignKey(Show, models.DO_NOTHING)
|
||||||
|
record = models.SmallIntegerField(blank=True, null=True)
|
||||||
|
rebroadcast = models.SmallIntegerField(blank=True, null=True)
|
||||||
|
instance = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
time_filled = models.DurationField(blank=True, null=True)
|
||||||
|
created = models.DateTimeField()
|
||||||
|
last_scheduled = models.DateTimeField(blank=True, null=True)
|
||||||
|
modified_instance = models.BooleanField()
|
||||||
|
autoplaylist_built = models.BooleanField()
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return show.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_show_instances'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowRebroadcast(models.Model):
|
||||||
|
day_offset = models.CharField(max_length=1024)
|
||||||
|
start_time = models.TimeField()
|
||||||
|
show = models.ForeignKey(Show, models.DO_NOTHING)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return show.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_show_rebroadcast'
|
|
@ -0,0 +1,66 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class SmartBlock(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
mtime = models.DateTimeField(blank=True, null=True)
|
||||||
|
utime = models.DateTimeField(blank=True, null=True)
|
||||||
|
creator = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
description = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
length = models.DurationField(blank=True, null=True)
|
||||||
|
type = models.CharField(max_length=7, blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.creator
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_block'
|
||||||
|
permissions = [
|
||||||
|
('change_own_smartblock', 'Change the smartblocks where they are the owner'),
|
||||||
|
('delete_own_smartblock', 'Delete the smartblocks where they are the owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SmartBlockContent(models.Model):
|
||||||
|
block = models.ForeignKey(SmartBlock, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
file = models.ForeignKey('File', models.DO_NOTHING, blank=True, null=True)
|
||||||
|
position = models.IntegerField(blank=True, null=True)
|
||||||
|
trackoffset = models.FloatField()
|
||||||
|
cliplength = models.DurationField(blank=True, null=True)
|
||||||
|
cuein = models.DurationField(blank=True, null=True)
|
||||||
|
cueout = models.DurationField(blank=True, null=True)
|
||||||
|
fadein = models.TimeField(blank=True, null=True)
|
||||||
|
fadeout = models.TimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.block.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_blockcontents'
|
||||||
|
permissions = [
|
||||||
|
('change_own_smartblockcontent', 'Change the content of smartblocks where they are the owner'),
|
||||||
|
('delete_own_smartblockcontent', 'Delete the content of smartblocks where they are the owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SmartBlockCriteria(models.Model):
|
||||||
|
criteria = models.CharField(max_length=32)
|
||||||
|
modifier = models.CharField(max_length=16)
|
||||||
|
value = models.CharField(max_length=512)
|
||||||
|
extra = models.CharField(max_length=512, blank=True, null=True)
|
||||||
|
criteriagroup = models.IntegerField(blank=True, null=True)
|
||||||
|
block = models.ForeignKey(SmartBlock, models.DO_NOTHING)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.block.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_blockcriteria'
|
||||||
|
permissions = [
|
||||||
|
('change_own_smartblockcriteria', 'Change the criteria of smartblocks where they are the owner'),
|
||||||
|
('delete_own_smartblockcriteria', 'Delete the criteria of smartblocks where they are the owner'),
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
from django.db import models
|
||||||
|
from .files import File
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdPartyTrackReference(models.Model):
|
||||||
|
service = models.CharField(max_length=256)
|
||||||
|
foreign_id = models.CharField(unique=True, max_length=256, blank=True, null=True)
|
||||||
|
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
|
||||||
|
upload_time = models.DateTimeField(blank=True, null=True)
|
||||||
|
status = models.CharField(max_length=256, blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'third_party_track_references'
|
||||||
|
|
||||||
|
class TrackType(models.Model):
|
||||||
|
code = models.CharField(max_length=16, unique=True)
|
||||||
|
type_name = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
description = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
visibility = models.BooleanField(blank=True, default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_track_types'
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
GUEST = 'G'
|
||||||
|
DJ = 'H'
|
||||||
|
PROGRAM_MANAGER = 'P'
|
||||||
|
ADMIN = 'A'
|
||||||
|
|
||||||
|
USER_TYPES = {
|
||||||
|
GUEST: 'Guest',
|
||||||
|
DJ: 'DJ',
|
||||||
|
PROGRAM_MANAGER: 'Program Manager',
|
||||||
|
ADMIN: 'Admin',
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from .schedule import Schedule
|
||||||
|
|
||||||
|
|
||||||
|
class Webstream(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.CharField(max_length=255)
|
||||||
|
url = models.CharField(max_length=512)
|
||||||
|
length = models.DurationField()
|
||||||
|
creator_id = models.IntegerField()
|
||||||
|
mtime = models.DateTimeField()
|
||||||
|
utime = models.DateTimeField()
|
||||||
|
lptime = models.DateTimeField(blank=True, null=True)
|
||||||
|
mime = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
User = get_user_model()
|
||||||
|
return User.objects.get(pk=self.creator_id)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_webstream'
|
||||||
|
permissions = [
|
||||||
|
('change_own_webstream', 'Change the webstreams where they are the owner'),
|
||||||
|
('delete_own_webstream', 'Delete the webstreams where they are the owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WebstreamMetadata(models.Model):
|
||||||
|
instance = models.ForeignKey(Schedule, models.DO_NOTHING)
|
||||||
|
start_time = models.DateTimeField()
|
||||||
|
liquidsoap_data = models.CharField(max_length=1024)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.instance.get_owner()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
db_table = 'cc_webstream_metadata'
|
|
@ -0,0 +1,104 @@
|
||||||
|
import logging
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from .models.user_constants import GUEST, DJ, PROGRAM_MANAGER, USER_TYPES
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
GUEST_PERMISSIONS = ['view_schedule',
|
||||||
|
'view_show',
|
||||||
|
'view_showdays',
|
||||||
|
'view_showhost',
|
||||||
|
'view_showinstance',
|
||||||
|
'view_showrebroadcast',
|
||||||
|
'view_file',
|
||||||
|
'view_podcast',
|
||||||
|
'view_podcastepisode',
|
||||||
|
'view_playlist',
|
||||||
|
'view_playlistcontent',
|
||||||
|
'view_smartblock',
|
||||||
|
'view_smartblockcontent',
|
||||||
|
'view_smartblockcriteria',
|
||||||
|
'view_webstream',
|
||||||
|
'view_apiroot',
|
||||||
|
]
|
||||||
|
DJ_PERMISSIONS = GUEST_PERMISSIONS + ['add_file',
|
||||||
|
'add_podcast',
|
||||||
|
'add_podcastepisode',
|
||||||
|
'add_playlist',
|
||||||
|
'add_playlistcontent',
|
||||||
|
'add_smartblock',
|
||||||
|
'add_smartblockcontent',
|
||||||
|
'add_smartblockcriteria',
|
||||||
|
'add_webstream',
|
||||||
|
'change_own_schedule',
|
||||||
|
'change_own_file',
|
||||||
|
'change_own_podcast',
|
||||||
|
'change_own_podcastepisode',
|
||||||
|
'change_own_playlist',
|
||||||
|
'change_own_playlistcontent',
|
||||||
|
'change_own_smartblock',
|
||||||
|
'change_own_smartblockcontent',
|
||||||
|
'change_own_smartblockcriteria',
|
||||||
|
'change_own_webstream',
|
||||||
|
'delete_own_schedule',
|
||||||
|
'delete_own_file',
|
||||||
|
'delete_own_podcast',
|
||||||
|
'delete_own_podcastepisode',
|
||||||
|
'delete_own_playlist',
|
||||||
|
'delete_own_playlistcontent',
|
||||||
|
'delete_own_smartblock',
|
||||||
|
'delete_own_smartblockcontent',
|
||||||
|
'delete_own_smartblockcriteria',
|
||||||
|
'delete_own_webstream',
|
||||||
|
]
|
||||||
|
PROGRAM_MANAGER_PERMISSIONS = GUEST_PERMISSIONS + ['add_show',
|
||||||
|
'add_showdays',
|
||||||
|
'add_showhost',
|
||||||
|
'add_showinstance',
|
||||||
|
'add_showrebroadcast',
|
||||||
|
'add_file',
|
||||||
|
'add_podcast',
|
||||||
|
'add_podcastepisode',
|
||||||
|
'add_playlist',
|
||||||
|
'add_playlistcontent',
|
||||||
|
'add_smartblock',
|
||||||
|
'add_smartblockcontent',
|
||||||
|
'add_smartblockcriteria',
|
||||||
|
'add_webstream',
|
||||||
|
'change_schedule',
|
||||||
|
'change_show',
|
||||||
|
'change_showdays',
|
||||||
|
'change_showhost',
|
||||||
|
'change_showinstance',
|
||||||
|
'change_showrebroadcast',
|
||||||
|
'change_file',
|
||||||
|
'change_podcast',
|
||||||
|
'change_podcastepisode',
|
||||||
|
'change_playlist',
|
||||||
|
'change_playlistcontent',
|
||||||
|
'change_smartblock',
|
||||||
|
'change_smartblockcontent',
|
||||||
|
'change_smartblockcriteria',
|
||||||
|
'change_webstream',
|
||||||
|
'delete_schedule',
|
||||||
|
'delete_show',
|
||||||
|
'delete_showdays',
|
||||||
|
'delete_showhost',
|
||||||
|
'delete_showinstance',
|
||||||
|
'delete_showrebroadcast',
|
||||||
|
'delete_file',
|
||||||
|
'delete_podcast',
|
||||||
|
'delete_podcastepisode',
|
||||||
|
'delete_playlist',
|
||||||
|
'delete_playlistcontent',
|
||||||
|
'delete_smartblock',
|
||||||
|
'delete_smartblockcontent',
|
||||||
|
'delete_smartblockcriteria',
|
||||||
|
'delete_webstream',
|
||||||
|
]
|
||||||
|
|
||||||
|
GROUPS = {
|
||||||
|
GUEST: GUEST_PERMISSIONS,
|
||||||
|
DJ: DJ_PERMISSIONS,
|
||||||
|
PROGRAM_MANAGER: PROGRAM_MANAGER,
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
from django.conf import settings
|
||||||
|
from .models.user_constants import DJ
|
||||||
|
|
||||||
|
REQUEST_PERMISSION_TYPE_MAP = {
|
||||||
|
'GET': 'view',
|
||||||
|
'HEAD': 'view',
|
||||||
|
'OPTIONS': 'view',
|
||||||
|
'POST': 'change',
|
||||||
|
'PUT': 'change',
|
||||||
|
'DELETE': 'delete',
|
||||||
|
'PATCH': 'change',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_own_obj(request, view):
|
||||||
|
user = request.user
|
||||||
|
if user is None or user.type != DJ:
|
||||||
|
return ''
|
||||||
|
if request.method == 'GET':
|
||||||
|
return ''
|
||||||
|
qs = view.queryset.all()
|
||||||
|
try:
|
||||||
|
model_owners = []
|
||||||
|
for model in qs:
|
||||||
|
owner = model.get_owner()
|
||||||
|
if owner not in model_owners:
|
||||||
|
model_owners.append(owner)
|
||||||
|
if len(model_owners) == 1 and user in model_owners:
|
||||||
|
return 'own_'
|
||||||
|
except AttributeError:
|
||||||
|
return ''
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_permission_for_view(request, view):
|
||||||
|
try:
|
||||||
|
permission_type = REQUEST_PERMISSION_TYPE_MAP[request.method]
|
||||||
|
if view.__class__.__name__ == 'APIRootView':
|
||||||
|
return '{}_apiroot'.format(permission_type)
|
||||||
|
model = view.model_permission_name
|
||||||
|
own_obj = get_own_obj(request, view)
|
||||||
|
return '{permission_type}_{own_obj}{model}'.format(permission_type=permission_type,
|
||||||
|
own_obj=own_obj,
|
||||||
|
model=model)
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_authorization_header(request):
|
||||||
|
auth_header = request.META.get('Authorization')
|
||||||
|
if not auth_header:
|
||||||
|
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||||
|
|
||||||
|
if auth_header.startswith('Api-Key'):
|
||||||
|
token = auth_header.split()[1]
|
||||||
|
if token == settings.CONFIG.get('general', 'api_key'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class IsAdminOrOwnUser(BasePermission):
|
||||||
|
"""
|
||||||
|
Implements Django Rest Framework permissions. This is separate from
|
||||||
|
Django's standard permission system. For details see
|
||||||
|
https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
|
||||||
|
"""
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if request.user.is_superuser():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if request.user.is_superuser():
|
||||||
|
return True
|
||||||
|
return obj.username == request.user
|
||||||
|
|
||||||
|
|
||||||
|
class IsSystemTokenOrUser(BasePermission):
|
||||||
|
"""
|
||||||
|
Implements Django Rest Framework permissions. This is separate from
|
||||||
|
Django's standard permission system. For details see
|
||||||
|
https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
|
||||||
|
|
||||||
|
This permission allows services (liquidsoap, 3rd-party, etc) to connect with
|
||||||
|
an API-Key header. All standard-users (i.e. not using the API-Key) have their
|
||||||
|
permissions checked against Django's standard permission system.
|
||||||
|
"""
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if request.user and request.user.is_authenticated:
|
||||||
|
perm = get_permission_for_view(request, view)
|
||||||
|
# Required as view_apiroot is a permission not linked to a specific
|
||||||
|
# model. This use-case allows users to view the base of the API
|
||||||
|
# explorer. Their assigned group permissions determine further access
|
||||||
|
# into the explorer.
|
||||||
|
if perm == 'view_apiroot':
|
||||||
|
return True
|
||||||
|
return request.user.has_perm(perm)
|
||||||
|
return check_authorization_header(request)
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if request.user and request.user.is_authenticated:
|
||||||
|
perm = get_permission_for_view(request, view)
|
||||||
|
return request.user.has_perm(perm, obj)
|
||||||
|
return check_authorization_header(request)
|
|
@ -0,0 +1,265 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = get_user_model()
|
||||||
|
fields = [
|
||||||
|
'item_url',
|
||||||
|
'username',
|
||||||
|
'type',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'lastfail',
|
||||||
|
'skype_contact',
|
||||||
|
'jabber_contact',
|
||||||
|
'email',
|
||||||
|
'cell_phone',
|
||||||
|
'login_attempts',
|
||||||
|
]
|
||||||
|
|
||||||
|
class SmartBlockSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SmartBlock
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class SmartBlockContentSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SmartBlockContent
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class SmartBlockCriteriaSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SmartBlockCriteria
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class CountrySerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class FileSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = File
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ListenerCountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ListenerCount
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class LiveLogSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = LiveLog
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class LoginAttemptSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = LoginAttempt
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class MountNameSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = MountName
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class MusicDirSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = MusicDir
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PlaylistSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Playlist
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PlaylistContentSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PlaylistContent
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PlayoutHistorySerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PlayoutHistory
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PlayoutHistoryMetadataSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PlayoutHistoryMetadata
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PlayoutHistoryTemplateSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PlayoutHistoryTemplate
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PlayoutHistoryTemplateFieldSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PlayoutHistoryTemplateField
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PreferenceSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Preference
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ScheduleSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
file_id = serializers.IntegerField(source='file.id', read_only=True)
|
||||||
|
stream_id = serializers.IntegerField(source='stream.id', read_only=True)
|
||||||
|
instance_id = serializers.IntegerField(source='instance.id', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Schedule
|
||||||
|
fields = [
|
||||||
|
'item_url',
|
||||||
|
'id',
|
||||||
|
'starts',
|
||||||
|
'ends',
|
||||||
|
'clip_length',
|
||||||
|
'fade_in',
|
||||||
|
'fade_out',
|
||||||
|
'cue_in',
|
||||||
|
'cue_out',
|
||||||
|
'media_item_played',
|
||||||
|
'file',
|
||||||
|
'file_id',
|
||||||
|
'stream',
|
||||||
|
'stream_id',
|
||||||
|
'instance',
|
||||||
|
'instance_id',
|
||||||
|
]
|
||||||
|
|
||||||
|
class ServiceRegisterSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ServiceRegister
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class SessionSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Session
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ShowSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Show
|
||||||
|
fields = [
|
||||||
|
'item_url',
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'url',
|
||||||
|
'genre',
|
||||||
|
'description',
|
||||||
|
'color',
|
||||||
|
'background_color',
|
||||||
|
'linked',
|
||||||
|
'is_linkable',
|
||||||
|
'image_path',
|
||||||
|
'has_autoplaylist',
|
||||||
|
'autoplaylist_repeat',
|
||||||
|
'autoplaylist',
|
||||||
|
]
|
||||||
|
|
||||||
|
class ShowDaysSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ShowDays
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ShowHostSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ShowHost
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ShowInstanceSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
show_id = serializers.IntegerField(source='show.id', read_only=True)
|
||||||
|
file_id = serializers.IntegerField(source='file.id', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = ShowInstance
|
||||||
|
fields = [
|
||||||
|
'item_url',
|
||||||
|
'id',
|
||||||
|
'description',
|
||||||
|
'starts',
|
||||||
|
'ends',
|
||||||
|
'record',
|
||||||
|
'rebroadcast',
|
||||||
|
'time_filled',
|
||||||
|
'created',
|
||||||
|
'last_scheduled',
|
||||||
|
'modified_instance',
|
||||||
|
'autoplaylist_built',
|
||||||
|
'show',
|
||||||
|
'show_id',
|
||||||
|
'instance',
|
||||||
|
'file',
|
||||||
|
'file_id',
|
||||||
|
]
|
||||||
|
|
||||||
|
class ShowRebroadcastSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ShowRebroadcast
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class StreamSettingSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = StreamSetting
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class UserTokenSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UserToken
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class TimestampSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Timestamp
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class WebstreamSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Webstream
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class WebstreamMetadataSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = WebstreamMetadata
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class CeleryTaskSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CeleryTask
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class CloudFileSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CloudFile
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ImportedPodcastSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ImportedPodcast
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PodcastSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Podcast
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class PodcastEpisodeSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PodcastEpisode
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class StationPodcastSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = StationPodcast
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ThirdPartyTrackReferenceSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ThirdPartyTrackReference
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class TrackTypeSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = TrackType
|
||||||
|
fields = '__all__'
|
|
@ -0,0 +1,184 @@
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
from .utils import read_config_file, get_random_string
|
||||||
|
|
||||||
|
LIBRETIME_CONF_DIR = os.getenv('LIBRETIME_CONF_DIR', '/etc/airtime')
|
||||||
|
DEFAULT_CONFIG_PATH = os.getenv('LIBRETIME_CONF_FILE',
|
||||||
|
os.path.join(LIBRETIME_CONF_DIR, 'airtime.conf'))
|
||||||
|
API_VERSION = '2.0.0'
|
||||||
|
|
||||||
|
try:
|
||||||
|
CONFIG = read_config_file(DEFAULT_CONFIG_PATH)
|
||||||
|
except IOError:
|
||||||
|
CONFIG = configparser.ConfigParser()
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = get_random_string(CONFIG.get('general', 'api_key', fallback=''))
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv('LIBRETIME_DEBUG', False)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'libretimeapi.apps.LibreTimeAPIConfig',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'rest_framework',
|
||||||
|
'url_filter',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'libretimeapi.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'libretimeapi.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': CONFIG.get('database', 'dbname', fallback=''),
|
||||||
|
'USER': CONFIG.get('database', 'dbuser', fallback=''),
|
||||||
|
'PASSWORD': CONFIG.get('database', 'dbpass', fallback=''),
|
||||||
|
'HOST': CONFIG.get('database', 'host', fallback=''),
|
||||||
|
'PORT': '5432',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'libretimeapi.permissions.IsSystemTokenOrUser',
|
||||||
|
],
|
||||||
|
'DEFAULT_FILTER_BACKENDS': [
|
||||||
|
'url_filter.integrations.drf.DjangoFilterBackend',
|
||||||
|
],
|
||||||
|
'URL_FIELD_NAME': 'item_url',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/api/static/'
|
||||||
|
if not DEBUG:
|
||||||
|
STATIC_ROOT = os.getenv('LIBRETIME_STATIC_ROOT', '/usr/share/airtime/api')
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'libretimeapi.User'
|
||||||
|
|
||||||
|
TEST_RUNNER = 'libretimeapi.tests.runners.ManagedModelTestRunner'
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'simple': {
|
||||||
|
'format': '{levelname} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
'verbose': {
|
||||||
|
'format': '{asctime} {module} {levelname} {message}',
|
||||||
|
'style': '{',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'file': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'filename': os.path.join(CONFIG.get('pypo', 'log_base_dir', fallback='.').replace('\'',''), 'api.log'),
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
|
'console': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'simple',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django': {
|
||||||
|
'handlers': ['file', 'console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propogate': True,
|
||||||
|
},
|
||||||
|
'libretimeapi': {
|
||||||
|
'handlers': ['file', 'console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propogate': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,24 @@
|
||||||
|
from django.test.runner import DiscoverRunner
|
||||||
|
|
||||||
|
|
||||||
|
class ManagedModelTestRunner(DiscoverRunner):
|
||||||
|
"""
|
||||||
|
Test runner that automatically makes all unmanaged models in your Django
|
||||||
|
project managed for the duration of the test run, so that one doesn't need
|
||||||
|
to execute the SQL manually to create them.
|
||||||
|
"""
|
||||||
|
def setup_test_environment(self, *args, **kwargs):
|
||||||
|
from django.apps import apps
|
||||||
|
self.unmanaged_models = [m for m in apps.get_models()
|
||||||
|
if not m._meta.managed]
|
||||||
|
for m in self.unmanaged_models:
|
||||||
|
m._meta.managed = True
|
||||||
|
super(ManagedModelTestRunner, self).setup_test_environment(*args,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def teardown_test_environment(self, *args, **kwargs):
|
||||||
|
super(ManagedModelTestRunner, self).teardown_test_environment(*args,
|
||||||
|
**kwargs)
|
||||||
|
# reset unmanaged models
|
||||||
|
for m in self.unmanaged_models:
|
||||||
|
m._meta.managed = False
|
|
@ -0,0 +1,40 @@
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.apps import apps
|
||||||
|
from libretimeapi.models import User
|
||||||
|
from libretimeapi.models.user_constants import GUEST, DJ
|
||||||
|
from libretimeapi.permission_constants import GROUPS
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserManager(APITestCase):
|
||||||
|
def test_create_user(self):
|
||||||
|
user = User.objects.create_user('test',
|
||||||
|
email='test@example.com',
|
||||||
|
password='test',
|
||||||
|
type=DJ,
|
||||||
|
first_name='test',
|
||||||
|
last_name='user')
|
||||||
|
db_user = User.objects.get(pk=user.pk)
|
||||||
|
self.assertEqual(db_user.username, user.username)
|
||||||
|
|
||||||
|
def test_create_superuser(self):
|
||||||
|
user = User.objects.create_superuser('test',
|
||||||
|
email='test@example.com',
|
||||||
|
password='test',
|
||||||
|
first_name='test',
|
||||||
|
last_name='user')
|
||||||
|
db_user = User.objects.get(pk=user.pk)
|
||||||
|
self.assertEqual(db_user.username, user.username)
|
||||||
|
|
||||||
|
class TestUser(APITestCase):
|
||||||
|
def test_guest_get_group_perms(self):
|
||||||
|
user = User.objects.create_user('test',
|
||||||
|
email='test@example.com',
|
||||||
|
password='test',
|
||||||
|
type=GUEST,
|
||||||
|
first_name='test',
|
||||||
|
last_name='user')
|
||||||
|
permissions = user.get_group_permissions()
|
||||||
|
# APIRoot permission hardcoded in the check as it isn't a Permission object
|
||||||
|
str_perms = [p.codename for p in permissions] + ['view_apiroot']
|
||||||
|
self.assertCountEqual(str_perms, GROUPS[GUEST])
|
|
@ -0,0 +1,119 @@
|
||||||
|
import os
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework.test import APITestCase, APIRequestFactory
|
||||||
|
from model_bakery import baker
|
||||||
|
from libretimeapi.permissions import IsSystemTokenOrUser
|
||||||
|
from libretimeapi.permission_constants import GUEST_PERMISSIONS, DJ_PERMISSIONS, PROGRAM_MANAGER_PERMISSIONS
|
||||||
|
from libretimeapi.models.user_constants import GUEST, DJ, PROGRAM_MANAGER, ADMIN
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsSystemTokenOrUser(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.path = "/api/v2/files/"
|
||||||
|
|
||||||
|
def test_unauthorized(self):
|
||||||
|
response = self.client.get(self.path.format('files'))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_token_incorrect(self):
|
||||||
|
token = 'doesnotexist'
|
||||||
|
request = APIRequestFactory().get(self.path)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
request.META['Authorization'] = 'Api-Key {token}'.format(token=token)
|
||||||
|
allowed = IsSystemTokenOrUser().has_permission(request, None)
|
||||||
|
self.assertFalse(allowed)
|
||||||
|
|
||||||
|
def test_token_correct(self):
|
||||||
|
token = settings.CONFIG.get('general', 'api_key')
|
||||||
|
request = APIRequestFactory().get(self.path)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
request.META['Authorization'] = 'Api-Key {token}'.format(token=token)
|
||||||
|
allowed = IsSystemTokenOrUser().has_permission(request, None)
|
||||||
|
self.assertTrue(allowed)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPermissions(APITestCase):
|
||||||
|
URLS = [
|
||||||
|
'schedule',
|
||||||
|
'shows',
|
||||||
|
'show-days',
|
||||||
|
'show-hosts',
|
||||||
|
'show-instances',
|
||||||
|
'show-rebroadcasts',
|
||||||
|
'files',
|
||||||
|
'playlists',
|
||||||
|
'playlist-contents',
|
||||||
|
'smart-blocks',
|
||||||
|
'smart-block-contents',
|
||||||
|
'smart-block-criteria',
|
||||||
|
'webstreams',
|
||||||
|
]
|
||||||
|
|
||||||
|
def logged_in_test_model(self, model, name, user_type, fn):
|
||||||
|
path = self.path.format(model)
|
||||||
|
user_created = get_user_model().objects.filter(username=name)
|
||||||
|
if not user_created:
|
||||||
|
user = get_user_model().objects.create_user(name,
|
||||||
|
email='test@example.com',
|
||||||
|
password='test',
|
||||||
|
type=user_type,
|
||||||
|
first_name='test',
|
||||||
|
last_name='user')
|
||||||
|
self.client.login(username=name, password='test')
|
||||||
|
return fn(path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.path = "/api/v2/{}/"
|
||||||
|
|
||||||
|
def test_guest_permissions_success(self):
|
||||||
|
for model in self.URLS:
|
||||||
|
response = self.logged_in_test_model(model, 'guest', GUEST, self.client.get)
|
||||||
|
self.assertEqual(response.status_code, 200,
|
||||||
|
msg='Invalid for model {}'.format(model))
|
||||||
|
|
||||||
|
def test_guest_permissions_failure(self):
|
||||||
|
for model in self.URLS:
|
||||||
|
response = self.logged_in_test_model(model, 'guest', GUEST, self.client.post)
|
||||||
|
self.assertEqual(response.status_code, 403,
|
||||||
|
msg='Invalid for model {}'.format(model))
|
||||||
|
response = self.logged_in_test_model('users', 'guest', GUEST, self.client.get)
|
||||||
|
self.assertEqual(response.status_code, 403, msg='Invalid for model users')
|
||||||
|
|
||||||
|
def test_dj_get_permissions(self):
|
||||||
|
for model in self.URLS:
|
||||||
|
response = self.logged_in_test_model(model, 'dj', DJ, self.client.get)
|
||||||
|
self.assertEqual(response.status_code, 200,
|
||||||
|
msg='Invalid for model {}'.format(model))
|
||||||
|
|
||||||
|
def test_dj_post_permissions(self):
|
||||||
|
user = get_user_model().objects.create_user('test-dj',
|
||||||
|
email='test@example.com',
|
||||||
|
password='test',
|
||||||
|
type=DJ,
|
||||||
|
first_name='test',
|
||||||
|
last_name='user')
|
||||||
|
f = baker.make('libretimeapi.File',
|
||||||
|
owner=user)
|
||||||
|
model = 'files/{}'.format(f.id)
|
||||||
|
path = self.path.format(model)
|
||||||
|
self.client.login(username='test-dj', password='test')
|
||||||
|
response = self.client.patch(path, {'name': 'newFilename'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_dj_post_permissions_failure(self):
|
||||||
|
user = get_user_model().objects.create_user('test-dj',
|
||||||
|
email='test@example.com',
|
||||||
|
password='test',
|
||||||
|
type=DJ,
|
||||||
|
first_name='test',
|
||||||
|
last_name='user')
|
||||||
|
f = baker.make('libretimeapi.File')
|
||||||
|
model = 'files/{}'.format(f.id)
|
||||||
|
path = self.path.format(model)
|
||||||
|
self.client.login(username='test-dj', password='test')
|
||||||
|
response = self.client.patch(path, {'name': 'newFilename'})
|
||||||
|
self.assertEqual(response.status_code, 403)
|
|
@ -0,0 +1,38 @@
|
||||||
|
import os
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework.test import APITestCase, APIRequestFactory
|
||||||
|
from model_bakery import baker
|
||||||
|
from libretimeapi.views import FileViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileViewSet(APITestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.path = "/api/v2/files/{id}/download/"
|
||||||
|
cls.token = settings.CONFIG.get('general', 'api_key')
|
||||||
|
|
||||||
|
def test_invalid(self):
|
||||||
|
path = self.path.format(id='a')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION='Api-Key {}'.format(self.token))
|
||||||
|
response = self.client.get(path)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_does_not_exist(self):
|
||||||
|
path = self.path.format(id='1')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION='Api-Key {}'.format(self.token))
|
||||||
|
response = self.client.get(path)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_exists(self):
|
||||||
|
music_dir = baker.make('libretimeapi.MusicDir',
|
||||||
|
directory=os.path.join(os.path.dirname(__file__),
|
||||||
|
'resources'))
|
||||||
|
f = baker.make('libretimeapi.File',
|
||||||
|
directory=music_dir,
|
||||||
|
mime='audio/mp3',
|
||||||
|
filepath='song.mp3')
|
||||||
|
path = self.path.format(id=str(f.pk))
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION='Api-Key {}'.format(self.token))
|
||||||
|
response = self.client.get(path)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
|
@ -0,0 +1,51 @@
|
||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register('smart-blocks', SmartBlockViewSet)
|
||||||
|
router.register('smart-block-contents', SmartBlockContentViewSet)
|
||||||
|
router.register('smart-block-criteria', SmartBlockCriteriaViewSet)
|
||||||
|
router.register('countries', CountryViewSet)
|
||||||
|
router.register('files', FileViewSet)
|
||||||
|
router.register('listener-counts', ListenerCountViewSet)
|
||||||
|
router.register('live-logs', LiveLogViewSet)
|
||||||
|
router.register('login-attempts', LoginAttemptViewSet)
|
||||||
|
router.register('mount-names', MountNameViewSet)
|
||||||
|
router.register('music-dirs', MusicDirViewSet)
|
||||||
|
router.register('playlists', PlaylistViewSet)
|
||||||
|
router.register('playlist-contents', PlaylistContentViewSet)
|
||||||
|
router.register('playout-history', PlayoutHistoryViewSet)
|
||||||
|
router.register('playout-history-metadata', PlayoutHistoryMetadataViewSet)
|
||||||
|
router.register('playout-history-templates', PlayoutHistoryTemplateViewSet)
|
||||||
|
router.register('playout-history-template-fields', PlayoutHistoryTemplateFieldViewSet)
|
||||||
|
router.register('preferences', PreferenceViewSet)
|
||||||
|
router.register('schedule', ScheduleViewSet)
|
||||||
|
router.register('service-registers', ServiceRegisterViewSet)
|
||||||
|
router.register('sessions', SessionViewSet)
|
||||||
|
router.register('shows', ShowViewSet)
|
||||||
|
router.register('show-days', ShowDaysViewSet)
|
||||||
|
router.register('show-hosts', ShowHostViewSet)
|
||||||
|
router.register('show-instances', ShowInstanceViewSet)
|
||||||
|
router.register('show-rebroadcasts', ShowRebroadcastViewSet)
|
||||||
|
router.register('stream-settings', StreamSettingViewSet)
|
||||||
|
router.register('users', UserViewSet)
|
||||||
|
router.register('user-tokens', UserTokenViewSet)
|
||||||
|
router.register('timestamps', TimestampViewSet)
|
||||||
|
router.register('webstreams', WebstreamViewSet)
|
||||||
|
router.register('webstream-metadata', WebstreamMetadataViewSet)
|
||||||
|
router.register('celery-tasks', CeleryTaskViewSet)
|
||||||
|
router.register('cloud-files', CloudFileViewSet)
|
||||||
|
router.register('imported-podcasts', ImportedPodcastViewSet)
|
||||||
|
router.register('podcasts', PodcastViewSet)
|
||||||
|
router.register('podcast-episodes', PodcastEpisodeViewSet)
|
||||||
|
router.register('station-podcasts', StationPodcastViewSet)
|
||||||
|
router.register('third-party-track-references', ThirdPartyTrackReferenceViewSet)
|
||||||
|
router.register('track-types', TrackTypeViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('api/v2/', include(router.urls)),
|
||||||
|
path('api/v2/version/', version),
|
||||||
|
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
import configparser
|
||||||
|
import sys
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
|
||||||
|
def read_config_file(config_path):
|
||||||
|
"""Parse the application's config file located at config_path."""
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
config.readfp(open(config_path))
|
||||||
|
except IOError as e:
|
||||||
|
print("Failed to open config file at {}: {}".format(config_path, e.strerror),
|
||||||
|
file=sys.stderr)
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
print(e.strerror, file=sys.stderr)
|
||||||
|
raise e
|
||||||
|
return config
|
||||||
|
|
||||||
|
def get_random_string(seed):
|
||||||
|
"""Generates a random string based on the given seed"""
|
||||||
|
choices = string.ascii_letters + string.digits + string.punctuation
|
||||||
|
seed = seed.encode('utf-8')
|
||||||
|
rand = random.Random(seed)
|
||||||
|
return [rand.choice(choices) for i in range(16)]
|
|
@ -0,0 +1,228 @@
|
||||||
|
import os
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import FileResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import api_view, action, permission_classes
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from .serializers import *
|
||||||
|
from .permissions import IsAdminOrOwnUser
|
||||||
|
|
||||||
|
class UserViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = get_user_model().objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [IsAdminOrOwnUser]
|
||||||
|
model_permission_name = 'user'
|
||||||
|
|
||||||
|
class SmartBlockViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = SmartBlock.objects.all()
|
||||||
|
serializer_class = SmartBlockSerializer
|
||||||
|
model_permission_name = 'smartblock'
|
||||||
|
|
||||||
|
class SmartBlockContentViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = SmartBlockContent.objects.all()
|
||||||
|
serializer_class = SmartBlockContentSerializer
|
||||||
|
model_permission_name = 'smartblockcontent'
|
||||||
|
|
||||||
|
class SmartBlockCriteriaViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = SmartBlockCriteria.objects.all()
|
||||||
|
serializer_class = SmartBlockCriteriaSerializer
|
||||||
|
model_permission_name = 'smartblockcriteria'
|
||||||
|
|
||||||
|
class CountryViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Country.objects.all()
|
||||||
|
serializer_class = CountrySerializer
|
||||||
|
model_permission_name = 'country'
|
||||||
|
|
||||||
|
class FileViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = File.objects.all()
|
||||||
|
serializer_class = FileSerializer
|
||||||
|
model_permission_name = 'file'
|
||||||
|
|
||||||
|
@action(detail=True, methods=['GET'])
|
||||||
|
def download(self, request, pk=None):
|
||||||
|
if pk is None:
|
||||||
|
return Response('No file requested', status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
pk = int(pk)
|
||||||
|
except ValueError:
|
||||||
|
return Response('File ID should be an integer',
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
filename = get_object_or_404(File, pk=pk)
|
||||||
|
directory = filename.directory
|
||||||
|
path = os.path.join(directory.directory, filename.filepath)
|
||||||
|
response = FileResponse(open(path, 'rb'), content_type=filename.mime)
|
||||||
|
return response
|
||||||
|
|
||||||
|
class ListenerCountViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ListenerCount.objects.all()
|
||||||
|
serializer_class = ListenerCountSerializer
|
||||||
|
model_permission_name = 'listenercount'
|
||||||
|
|
||||||
|
class LiveLogViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = LiveLog.objects.all()
|
||||||
|
serializer_class = LiveLogSerializer
|
||||||
|
model_permission_name = 'livelog'
|
||||||
|
|
||||||
|
class LoginAttemptViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = LoginAttempt.objects.all()
|
||||||
|
serializer_class = LoginAttemptSerializer
|
||||||
|
model_permission_name = 'loginattempt'
|
||||||
|
|
||||||
|
class MountNameViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = MountName.objects.all()
|
||||||
|
serializer_class = MountNameSerializer
|
||||||
|
model_permission_name = 'mountname'
|
||||||
|
|
||||||
|
class MusicDirViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = MusicDir.objects.all()
|
||||||
|
serializer_class = MusicDirSerializer
|
||||||
|
model_permission_name = 'musicdir'
|
||||||
|
|
||||||
|
class PlaylistViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Playlist.objects.all()
|
||||||
|
serializer_class = PlaylistSerializer
|
||||||
|
model_permission_name = 'playlist'
|
||||||
|
|
||||||
|
class PlaylistContentViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PlaylistContent.objects.all()
|
||||||
|
serializer_class = PlaylistContentSerializer
|
||||||
|
model_permission_name = 'playlistcontent'
|
||||||
|
|
||||||
|
class PlayoutHistoryViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PlayoutHistory.objects.all()
|
||||||
|
serializer_class = PlayoutHistorySerializer
|
||||||
|
model_permission_name = 'playouthistory'
|
||||||
|
|
||||||
|
class PlayoutHistoryMetadataViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PlayoutHistoryMetadata.objects.all()
|
||||||
|
serializer_class = PlayoutHistoryMetadataSerializer
|
||||||
|
model_permission_name = 'playouthistorymetadata'
|
||||||
|
|
||||||
|
class PlayoutHistoryTemplateViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PlayoutHistoryTemplate.objects.all()
|
||||||
|
serializer_class = PlayoutHistoryTemplateSerializer
|
||||||
|
model_permission_name = 'playouthistorytemplate'
|
||||||
|
|
||||||
|
class PlayoutHistoryTemplateFieldViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PlayoutHistoryTemplateField.objects.all()
|
||||||
|
serializer_class = PlayoutHistoryTemplateFieldSerializer
|
||||||
|
model_permission_name = 'playouthistorytemplatefield'
|
||||||
|
|
||||||
|
class PreferenceViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Preference.objects.all()
|
||||||
|
serializer_class = PreferenceSerializer
|
||||||
|
model_permission_name = 'perference'
|
||||||
|
|
||||||
|
class ScheduleViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Schedule.objects.all()
|
||||||
|
serializer_class = ScheduleSerializer
|
||||||
|
filter_fields = ('starts', 'ends', 'playout_status', 'broadcasted')
|
||||||
|
model_permission_name = 'schedule'
|
||||||
|
|
||||||
|
class ServiceRegisterViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ServiceRegister.objects.all()
|
||||||
|
serializer_class = ServiceRegisterSerializer
|
||||||
|
model_permission_name = 'serviceregister'
|
||||||
|
|
||||||
|
class SessionViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Session.objects.all()
|
||||||
|
serializer_class = SessionSerializer
|
||||||
|
model_permission_name = 'session'
|
||||||
|
|
||||||
|
class ShowViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Show.objects.all()
|
||||||
|
serializer_class = ShowSerializer
|
||||||
|
model_permission_name = 'show'
|
||||||
|
|
||||||
|
class ShowDaysViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ShowDays.objects.all()
|
||||||
|
serializer_class = ShowDaysSerializer
|
||||||
|
model_permission_name = 'showdays'
|
||||||
|
|
||||||
|
class ShowHostViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ShowHost.objects.all()
|
||||||
|
serializer_class = ShowHostSerializer
|
||||||
|
model_permission_name = 'showhost'
|
||||||
|
|
||||||
|
class ShowInstanceViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ShowInstance.objects.all()
|
||||||
|
serializer_class = ShowInstanceSerializer
|
||||||
|
model_permission_name = 'showinstance'
|
||||||
|
|
||||||
|
class ShowRebroadcastViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ShowRebroadcast.objects.all()
|
||||||
|
serializer_class = ShowRebroadcastSerializer
|
||||||
|
model_permission_name = 'showrebroadcast'
|
||||||
|
|
||||||
|
class StreamSettingViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = StreamSetting.objects.all()
|
||||||
|
serializer_class = StreamSettingSerializer
|
||||||
|
model_permission_name = 'streamsetting'
|
||||||
|
|
||||||
|
class UserTokenViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = UserToken.objects.all()
|
||||||
|
serializer_class = UserTokenSerializer
|
||||||
|
model_permission_name = 'usertoken'
|
||||||
|
|
||||||
|
class TimestampViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Timestamp.objects.all()
|
||||||
|
serializer_class = TimestampSerializer
|
||||||
|
model_permission_name = 'timestamp'
|
||||||
|
|
||||||
|
class WebstreamViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Webstream.objects.all()
|
||||||
|
serializer_class = WebstreamSerializer
|
||||||
|
model_permission_name = 'webstream'
|
||||||
|
|
||||||
|
class WebstreamMetadataViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = WebstreamMetadata.objects.all()
|
||||||
|
serializer_class = WebstreamMetadataSerializer
|
||||||
|
model_permission_name = 'webstreametadata'
|
||||||
|
|
||||||
|
class CeleryTaskViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = CeleryTask.objects.all()
|
||||||
|
serializer_class = CeleryTaskSerializer
|
||||||
|
model_permission_name = 'celerytask'
|
||||||
|
|
||||||
|
class CloudFileViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = CloudFile.objects.all()
|
||||||
|
serializer_class = CloudFileSerializer
|
||||||
|
model_permission_name = 'cloudfile'
|
||||||
|
|
||||||
|
class ImportedPodcastViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ImportedPodcast.objects.all()
|
||||||
|
serializer_class = ImportedPodcastSerializer
|
||||||
|
model_permission_name = 'importedpodcast'
|
||||||
|
|
||||||
|
class PodcastViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Podcast.objects.all()
|
||||||
|
serializer_class = PodcastSerializer
|
||||||
|
model_permission_name = 'podcast'
|
||||||
|
|
||||||
|
class PodcastEpisodeViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PodcastEpisode.objects.all()
|
||||||
|
serializer_class = PodcastEpisodeSerializer
|
||||||
|
model_permission_name = 'podcastepisode'
|
||||||
|
|
||||||
|
class StationPodcastViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = StationPodcast.objects.all()
|
||||||
|
serializer_class = StationPodcastSerializer
|
||||||
|
model_permission_name = 'station'
|
||||||
|
|
||||||
|
class ThirdPartyTrackReferenceViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ThirdPartyTrackReference.objects.all()
|
||||||
|
serializer_class = ThirdPartyTrackReferenceSerializer
|
||||||
|
model_permission_name = 'thirdpartytrackreference'
|
||||||
|
|
||||||
|
class TrackTypeViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = TrackType.objects.all()
|
||||||
|
serializer_class = TrackTypeSerializer
|
||||||
|
model_permission_name = 'tracktype'
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes((AllowAny, ))
|
||||||
|
def version(request, *args, **kwargs):
|
||||||
|
return Response({'api_version': settings.API_VERSION})
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for api project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'libretimeapi.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
|
@ -0,0 +1,32 @@
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
script_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
print(script_path)
|
||||||
|
os.chdir(script_path)
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='libretime-api',
|
||||||
|
version='2.0.0a1',
|
||||||
|
packages=find_packages(),
|
||||||
|
include_package_data=True,
|
||||||
|
description='LibreTime API backend server',
|
||||||
|
url='https://github.com/LibreTime/libretime',
|
||||||
|
author='LibreTime Contributors',
|
||||||
|
scripts=['bin/libretime-api'],
|
||||||
|
install_requires=[
|
||||||
|
'coreapi',
|
||||||
|
'Django~=3.0',
|
||||||
|
'djangorestframework',
|
||||||
|
'django-url-filter',
|
||||||
|
'markdown',
|
||||||
|
'model_bakery',
|
||||||
|
'psycopg2',
|
||||||
|
],
|
||||||
|
project_urls={
|
||||||
|
'Bug Tracker': 'https://github.com/LibreTime/libretime/issues',
|
||||||
|
'Documentation': 'https://libretime.org',
|
||||||
|
'Source Code': 'https://github.com/LibreTime/libretime',
|
||||||
|
},
|
||||||
|
)
|
25
install
25
install
|
@ -202,6 +202,11 @@ function systemInitInstall() {
|
||||||
target_path="/etc/systemd/system/${service_name}.service"
|
target_path="/etc/systemd/system/${service_name}.service"
|
||||||
alt_path=$(echo $target_path | sed 's/libretime-/airtime-/')
|
alt_path=$(echo $target_path | sed 's/libretime-/airtime-/')
|
||||||
;;
|
;;
|
||||||
|
libretime-api)
|
||||||
|
source_path="${SCRIPT_DIR-$PWD}/api/install/systemd/${service_name}.service"
|
||||||
|
target_path="/etc/systemd/system/${service_name}.service"
|
||||||
|
alt_path=$(echo $target_path | sed 's/libretime-/airtime-/')
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
if [[ ! -e $source_path ]]; then
|
if [[ ! -e $source_path ]]; then
|
||||||
echo "$0:${FUNCNAME}(): ERROR: service \"$service_name\" with source path \"$source_path\" does not exist!" >&2
|
echo "$0:${FUNCNAME}(): ERROR: service \"$service_name\" with source path \"$source_path\" does not exist!" >&2
|
||||||
|
@ -1019,6 +1024,16 @@ loudCmd "$python_bin ${AIRTIMEROOT}/python_apps/airtime_analyzer/setup.py instal
|
||||||
systemInitInstall libretime-analyzer $web_user
|
systemInitInstall libretime-analyzer $web_user
|
||||||
verbose "...Done"
|
verbose "...Done"
|
||||||
|
|
||||||
|
verbose "\n * Installing API..."
|
||||||
|
loudCmd "python3 ${AIRTIMEROOT}/api/setup.py install --install-scripts=/usr/bin"
|
||||||
|
systemInitInstall libretime-api $web_user
|
||||||
|
mkdir -p /etc/airtime
|
||||||
|
sed -e "s@WEB_USER@${web_user}@g" \
|
||||||
|
-e "s@WEB_ROOT@${web_root}@g" \
|
||||||
|
${AIRTIMEROOT}/installer/uwsgi/libretime-api.ini > /etc/airtime/libretime-api.ini
|
||||||
|
loudCmd "libretime-api collectstatic --clear --noinput"
|
||||||
|
verbose "...Done"
|
||||||
|
|
||||||
verbose "\n * Setting permissions on /var/log/airtime..."
|
verbose "\n * Setting permissions on /var/log/airtime..."
|
||||||
# Make the airtime log directory group-writable
|
# Make the airtime log directory group-writable
|
||||||
loudCmd "chmod -R 775 /var/log/airtime"
|
loudCmd "chmod -R 775 /var/log/airtime"
|
||||||
|
@ -1058,15 +1073,15 @@ fi
|
||||||
|
|
||||||
# Enable Apache modules
|
# Enable Apache modules
|
||||||
if $is_debian_buster; then
|
if $is_debian_buster; then
|
||||||
loudCmd "a2enmod rewrite php7.3"
|
loudCmd "a2enmod rewrite php7.3 proxy proxy_http"
|
||||||
elif $is_ubuntu_bionic; then
|
elif $is_ubuntu_bionic; then
|
||||||
loudCmd "a2enmod rewrite php7.2"
|
loudCmd "a2enmod rewrite php7.2 proxy proxy_http"
|
||||||
elif $is_ubuntu_xenial || $is_debian_stretch; then
|
elif $is_ubuntu_xenial || $is_debian_stretch; then
|
||||||
loudCmd "a2enmod rewrite php7.0"
|
loudCmd "a2enmod rewrite php7.0 proxy proxy_http"
|
||||||
elif $is_centos_dist; then
|
elif $is_centos_dist; then
|
||||||
verbose "TODO: enable Apache modules mod_rewrite and mod_php manually"
|
verbose "TODO: enable Apache modules mod_rewrite, mod_php, mod_proxy and mod_proxy_http manually"
|
||||||
else
|
else
|
||||||
loudCmd "a2enmod rewrite php5"
|
loudCmd "a2enmod rewrite php5 proxy proxy_http"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $skip_postgres -eq 0 ]; then
|
if [ $skip_postgres -eq 0 ]; then
|
||||||
|
|
|
@ -1,29 +1,34 @@
|
||||||
WEB_PORT_LISTEN
|
WEB_PORT_LISTEN
|
||||||
|
|
||||||
<VirtualHost *:WEB_PORT>
|
<VirtualHost *:WEB_PORT>
|
||||||
ServerAdmin foo@bar.org
|
ServerAdmin foo@bar.org
|
||||||
DocumentRoot WEB_ROOT
|
DocumentRoot WEB_ROOT
|
||||||
php_admin_value upload_tmp_dir /tmp
|
php_admin_value upload_tmp_dir /tmp
|
||||||
php_value post_max_size 500M
|
php_value post_max_size 500M
|
||||||
php_value upload_max_filesize 500M
|
php_value upload_max_filesize 500M
|
||||||
php_value request_order "GPC"
|
php_value request_order "GPC"
|
||||||
php_value session.gc_probability 0
|
php_value session.gc_probability 0
|
||||||
php_value session.auto_start 0
|
php_value session.auto_start 0
|
||||||
|
|
||||||
AddOutputFilterByType DEFLATE text/plain
|
AddOutputFilterByType DEFLATE text/plain
|
||||||
AddOutputFilterByType DEFLATE text/html
|
AddOutputFilterByType DEFLATE text/html
|
||||||
AddOutputFilterByType DEFLATE text/xml
|
AddOutputFilterByType DEFLATE text/xml
|
||||||
AddOutputFilterByType DEFLATE text/css
|
AddOutputFilterByType DEFLATE text/css
|
||||||
AddOutputFilterByType DEFLATE application/xml
|
AddOutputFilterByType DEFLATE application/xml
|
||||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||||
AddOutputFilterByType DEFLATE application/rss+xml
|
AddOutputFilterByType DEFLATE application/rss+xml
|
||||||
AddOutputFilterByType DEFLATE application/javascript
|
AddOutputFilterByType DEFLATE application/javascript
|
||||||
AddOutputFilterByType DEFLATE application/x-javascript
|
AddOutputFilterByType DEFLATE application/x-javascript
|
||||||
AddOutputFilterByType DEFLATE application/json
|
AddOutputFilterByType DEFLATE application/json
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /api/v2/ http://localhost:8081/api/v2/
|
||||||
|
ProxyPassReverse /api/v2/ http://localhost:8081/api/v2/
|
||||||
|
ProxyPass /api-auth/ http://localhost:8081/api-auth/
|
||||||
|
ProxyPassReverse /api-auth/ http://localhost:8081/api-auth/
|
||||||
|
Alias /api/static /usr/share/airtime/api/
|
||||||
|
|
||||||
<Directory WEB_ROOT>
|
<Directory WEB_ROOT>
|
||||||
|
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteCond %{REQUEST_FILENAME} -s [OR]
|
RewriteCond %{REQUEST_FILENAME} -s [OR]
|
||||||
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||||
|
@ -34,6 +39,10 @@ WEB_PORT_LISTEN
|
||||||
Allow from all
|
Allow from all
|
||||||
|
|
||||||
Require all granted
|
Require all granted
|
||||||
|
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
|
<Directory /usr/share/airtime/api>
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
|
@ -1,26 +1,32 @@
|
||||||
WEB_PORT_LISTEN
|
WEB_PORT_LISTEN
|
||||||
|
|
||||||
<VirtualHost *:WEB_PORT>
|
<VirtualHost *:WEB_PORT>
|
||||||
ServerAdmin foo@bar.org
|
ServerAdmin foo@bar.org
|
||||||
DocumentRoot WEB_ROOT
|
DocumentRoot WEB_ROOT
|
||||||
php_admin_value upload_tmp_dir /tmp
|
php_admin_value upload_tmp_dir /tmp
|
||||||
php_value post_max_size 500M
|
php_value post_max_size 500M
|
||||||
php_value upload_max_filesize 500M
|
php_value upload_max_filesize 500M
|
||||||
php_value request_order "GPC"
|
php_value request_order "GPC"
|
||||||
php_value session.gc_probability 0
|
php_value session.gc_probability 0
|
||||||
php_value session.auto_start 0
|
php_value session.auto_start 0
|
||||||
|
|
||||||
AddOutputFilterByType DEFLATE text/plain
|
AddOutputFilterByType DEFLATE text/plain
|
||||||
AddOutputFilterByType DEFLATE text/html
|
AddOutputFilterByType DEFLATE text/html
|
||||||
AddOutputFilterByType DEFLATE text/xml
|
AddOutputFilterByType DEFLATE text/xml
|
||||||
AddOutputFilterByType DEFLATE text/css
|
AddOutputFilterByType DEFLATE text/css
|
||||||
AddOutputFilterByType DEFLATE application/xml
|
AddOutputFilterByType DEFLATE application/xml
|
||||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||||
AddOutputFilterByType DEFLATE application/rss+xml
|
AddOutputFilterByType DEFLATE application/rss+xml
|
||||||
AddOutputFilterByType DEFLATE application/javascript
|
AddOutputFilterByType DEFLATE application/javascript
|
||||||
AddOutputFilterByType DEFLATE application/x-javascript
|
AddOutputFilterByType DEFLATE application/x-javascript
|
||||||
AddOutputFilterByType DEFLATE application/json
|
AddOutputFilterByType DEFLATE application/json
|
||||||
|
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /api/v2/ http://localhost:8081/api/v2/
|
||||||
|
ProxyPassReverse /api/v2/ http://localhost:8081/api/v2/
|
||||||
|
ProxyPass /api-auth/ http://localhost:8081/api-auth/
|
||||||
|
ProxyPassReverse /api-auth/ http://localhost:8081/api-auth/
|
||||||
|
Alias /api/static /usr/share/airtime/api/
|
||||||
|
|
||||||
<Directory WEB_ROOT>
|
<Directory WEB_ROOT>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
@ -37,4 +43,8 @@ WEB_PORT_LISTEN
|
||||||
|
|
||||||
Require all granted
|
Require all granted
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
|
<Directory /usr/share/airtime/api>
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
|
@ -22,6 +22,7 @@ libmad-ocaml
|
||||||
libopus0
|
libopus0
|
||||||
libportaudio2
|
libportaudio2
|
||||||
libpulse0
|
libpulse0
|
||||||
|
libpq-dev
|
||||||
libsamplerate0
|
libsamplerate0
|
||||||
libsoundtouch-ocaml
|
libsoundtouch-ocaml
|
||||||
libtaglib-ocaml
|
libtaglib-ocaml
|
||||||
|
@ -58,6 +59,8 @@ rabbitmq-server
|
||||||
silan
|
silan
|
||||||
systemd-sysv
|
systemd-sysv
|
||||||
unzip
|
unzip
|
||||||
|
uwsgi
|
||||||
|
uwsgi-plugin-python3
|
||||||
vorbisgain
|
vorbisgain
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
|
|
|
@ -17,6 +17,7 @@ libmad-ocaml
|
||||||
libopus0
|
libopus0
|
||||||
libportaudio2
|
libportaudio2
|
||||||
libpulse0
|
libpulse0
|
||||||
|
libpq-dev
|
||||||
libsamplerate0
|
libsamplerate0
|
||||||
libsoundtouch-ocaml
|
libsoundtouch-ocaml
|
||||||
libtaglib-ocaml
|
libtaglib-ocaml
|
||||||
|
@ -49,6 +50,8 @@ python3-cairo
|
||||||
rabbitmq-server
|
rabbitmq-server
|
||||||
systemd-sysv
|
systemd-sysv
|
||||||
unzip
|
unzip
|
||||||
|
uwsgi
|
||||||
|
uwsgi-plugin-python3
|
||||||
vorbisgain
|
vorbisgain
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
|
|
|
@ -23,6 +23,7 @@ libmad-ocaml
|
||||||
libopus0
|
libopus0
|
||||||
libportaudio2
|
libportaudio2
|
||||||
libpulse0
|
libpulse0
|
||||||
|
libpq-dev
|
||||||
libsamplerate0
|
libsamplerate0
|
||||||
libsoundtouch-ocaml
|
libsoundtouch-ocaml
|
||||||
libssl-dev
|
libssl-dev
|
||||||
|
@ -70,6 +71,8 @@ rabbitmq-server
|
||||||
silan
|
silan
|
||||||
sysvinit-utils
|
sysvinit-utils
|
||||||
unzip
|
unzip
|
||||||
|
uwsgi
|
||||||
|
uwsgi-plugin-python3
|
||||||
vorbisgain
|
vorbisgain
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
xmlstarlet
|
xmlstarlet
|
||||||
|
|
|
@ -23,6 +23,7 @@ libmad-ocaml
|
||||||
libopus0
|
libopus0
|
||||||
libportaudio2
|
libportaudio2
|
||||||
libpulse0
|
libpulse0
|
||||||
|
libpq-dev
|
||||||
libsamplerate0
|
libsamplerate0
|
||||||
libsoundtouch-ocaml
|
libsoundtouch-ocaml
|
||||||
libssl-dev
|
libssl-dev
|
||||||
|
@ -70,6 +71,8 @@ rabbitmq-server
|
||||||
silan
|
silan
|
||||||
sysvinit-utils
|
sysvinit-utils
|
||||||
unzip
|
unzip
|
||||||
|
uwsgi
|
||||||
|
uwsgi-plugin-python3
|
||||||
vorbisgain
|
vorbisgain
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
|
|
|
@ -17,6 +17,7 @@ libmad-ocaml
|
||||||
libopus0
|
libopus0
|
||||||
libportaudio2
|
libportaudio2
|
||||||
libpulse0
|
libpulse0
|
||||||
|
libpq-dev
|
||||||
libsamplerate0
|
libsamplerate0
|
||||||
libsoundtouch-ocaml
|
libsoundtouch-ocaml
|
||||||
libssl-dev
|
libssl-dev
|
||||||
|
@ -63,4 +64,6 @@ vorbisgain
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
vorbis-tools
|
vorbis-tools
|
||||||
xmlstarlet
|
xmlstarlet
|
||||||
|
uwsgi
|
||||||
|
uwsgi-plugin-python3
|
||||||
zip
|
zip
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
[uwsgi]
|
||||||
|
module = libretimeapi.wsgi
|
||||||
|
|
||||||
|
master = true
|
||||||
|
plugin = python3
|
||||||
|
processes = 10
|
||||||
|
vacuum = true
|
||||||
|
http-socket = :8081
|
||||||
|
uid = WEB_USER
|
||||||
|
gid = WEB_USER
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__all__ = ["api_client"]
|
__all__ = ["version1"]
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import requests
|
||||||
|
from requests.auth import AuthBase
|
||||||
|
|
||||||
|
def get_protocol(config):
|
||||||
|
positive_values = ['Yes', 'yes', 'True', 'true', True]
|
||||||
|
port = config['general'].get('base_port', 80)
|
||||||
|
force_ssl = config['general'].get('force_ssl', False)
|
||||||
|
if force_ssl in positive_values:
|
||||||
|
protocol = 'https'
|
||||||
|
else:
|
||||||
|
protocol = config['general'].get('protocol')
|
||||||
|
if not protocol:
|
||||||
|
protocol = str(("http", "https")[int(port) == 443])
|
||||||
|
return protocol
|
||||||
|
|
||||||
|
class UrlParamDict(dict):
|
||||||
|
def __missing__(self, key):
|
||||||
|
return '{' + key + '}'
|
||||||
|
|
||||||
|
class UrlException(Exception): pass
|
||||||
|
|
||||||
|
class IncompleteUrl(UrlException):
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Incomplete url: '{}'".format(self.url)
|
||||||
|
|
||||||
|
class UrlBadParam(UrlException):
|
||||||
|
def __init__(self, url, param):
|
||||||
|
self.url = url
|
||||||
|
self.param = param
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Bad param '{}' passed into url: '{}'".format(self.param, self.url)
|
||||||
|
|
||||||
|
class KeyAuth(AuthBase):
|
||||||
|
def __init__(self, key):
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
def __call__(self, r):
|
||||||
|
r.headers['Authorization'] = "Api-Key {}".format(self.key)
|
||||||
|
return r
|
||||||
|
|
||||||
|
class ApcUrl:
|
||||||
|
""" A safe abstraction and testable for filling in parameters in
|
||||||
|
api_client.cfg"""
|
||||||
|
def __init__(self, base_url):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def params(self, **params):
|
||||||
|
temp_url = self.base_url
|
||||||
|
for k, v in params.items():
|
||||||
|
wrapped_param = "{" + k + "}"
|
||||||
|
if not wrapped_param in temp_url:
|
||||||
|
raise UrlBadParam(self.base_url, k)
|
||||||
|
temp_url = temp_url.format_map(UrlParamDict(**params))
|
||||||
|
return ApcUrl(temp_url)
|
||||||
|
|
||||||
|
def url(self):
|
||||||
|
if '{' in self.base_url:
|
||||||
|
raise IncompleteUrl(self.base_url)
|
||||||
|
else:
|
||||||
|
return self.base_url
|
||||||
|
|
||||||
|
class ApiRequest:
|
||||||
|
API_HTTP_REQUEST_TIMEOUT = 30 # 30 second HTTP request timeout
|
||||||
|
|
||||||
|
def __init__(self, name, url, logger=None, api_key=None):
|
||||||
|
self.name = name
|
||||||
|
self.url = url
|
||||||
|
self.__req = None
|
||||||
|
if logger is None:
|
||||||
|
self.logger = logging
|
||||||
|
else:
|
||||||
|
self.logger = logger
|
||||||
|
self.auth = KeyAuth(api_key)
|
||||||
|
|
||||||
|
def __call__(self, _post_data=None, params=None, **kwargs):
|
||||||
|
final_url = self.url.params(**kwargs).url()
|
||||||
|
self.logger.debug(final_url)
|
||||||
|
try:
|
||||||
|
if _post_data:
|
||||||
|
response = requests.post(final_url,
|
||||||
|
data=_post_data, auth=self.auth,
|
||||||
|
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT)
|
||||||
|
else:
|
||||||
|
response = requests.get(final_url, params=params, auth=self.auth,
|
||||||
|
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT)
|
||||||
|
if 'application/json' in response.headers['content-type']:
|
||||||
|
return response.json()
|
||||||
|
return response
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
self.logger.error('HTTP request to %s timed out', final_url)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def req(self, *args, **kwargs):
|
||||||
|
self.__req = lambda : self(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def retry(self, n, delay=5):
|
||||||
|
"""Try to send request n times. If after n times it fails then
|
||||||
|
we finally raise exception"""
|
||||||
|
for i in range(0,n-1):
|
||||||
|
try:
|
||||||
|
return self.__req()
|
||||||
|
except Exception:
|
||||||
|
time.sleep(delay)
|
||||||
|
return self.__req()
|
||||||
|
|
||||||
|
class RequestProvider:
|
||||||
|
""" Creates the available ApiRequest instance that can be read from
|
||||||
|
a config file """
|
||||||
|
def __init__(self, cfg, endpoints):
|
||||||
|
self.config = cfg
|
||||||
|
self.requests = {}
|
||||||
|
if self.config["general"]["base_dir"].startswith("/"):
|
||||||
|
self.config["general"]["base_dir"] = self.config["general"]["base_dir"][1:]
|
||||||
|
|
||||||
|
protocol = get_protocol(self.config)
|
||||||
|
base_port = self.config['general']['base_port']
|
||||||
|
base_url = self.config['general']['base_url']
|
||||||
|
base_dir = self.config['general']['base_dir']
|
||||||
|
api_base = self.config['api_base']
|
||||||
|
api_url = "{protocol}://{base_url}:{base_port}/{base_dir}{api_base}/{action}".format_map(
|
||||||
|
UrlParamDict(protocol=protocol,
|
||||||
|
base_url=base_url,
|
||||||
|
base_port=base_port,
|
||||||
|
base_dir=base_dir,
|
||||||
|
api_base=api_base
|
||||||
|
))
|
||||||
|
self.url = ApcUrl(api_url)
|
||||||
|
|
||||||
|
# Now we must discover the possible actions
|
||||||
|
for action_name, action_value in endpoints.items():
|
||||||
|
new_url = self.url.params(action=action_value)
|
||||||
|
if '{api_key}' in action_value:
|
||||||
|
new_url = new_url.params(api_key=self.config["general"]['api_key'])
|
||||||
|
self.requests[action_name] = ApiRequest(action_name,
|
||||||
|
new_url,
|
||||||
|
api_key=self.config['general']['api_key'])
|
||||||
|
|
||||||
|
def available_requests(self):
|
||||||
|
return list(self.requests.keys())
|
||||||
|
|
||||||
|
def __contains__(self, request):
|
||||||
|
return request in self.requests
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
if attr in self:
|
||||||
|
return self.requests[attr]
|
||||||
|
else:
|
||||||
|
return super(RequestProvider, self).__getattribute__(attr)
|
||||||
|
|
||||||
|
def time_in_seconds(time):
|
||||||
|
return time.hour * 60 * 60 + \
|
||||||
|
time.minute * 60 + \
|
||||||
|
time.second + \
|
||||||
|
time.microsecond / 1000000.0
|
||||||
|
|
||||||
|
def time_in_milliseconds(time):
|
||||||
|
return time_in_seconds(time) * 1000
|
||||||
|
|
|
@ -8,202 +8,71 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.request, urllib.error, urllib.parse
|
import urllib.parse
|
||||||
import requests
|
import requests
|
||||||
import socket
|
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
import traceback
|
import traceback
|
||||||
from configobj import ConfigObj
|
from configobj import ConfigObj
|
||||||
|
|
||||||
|
from .utils import RequestProvider, ApiRequest, get_protocol
|
||||||
|
|
||||||
AIRTIME_API_VERSION = "1.1"
|
AIRTIME_API_VERSION = "1.1"
|
||||||
|
|
||||||
|
|
||||||
api_config = {}
|
api_config = {}
|
||||||
|
api_endpoints = {}
|
||||||
|
|
||||||
# URL to get the version number of the server API
|
# URL to get the version number of the server API
|
||||||
api_config['version_url'] = 'version/api_key/%%api_key%%'
|
api_endpoints['version_url'] = 'version/api_key/{api_key}'
|
||||||
#URL to register a components IP Address with the central web server
|
#URL to register a components IP Address with the central web server
|
||||||
api_config['register_component'] = 'register-component/format/json/api_key/%%api_key%%/component/%%component%%'
|
api_endpoints['register_component'] = 'register-component/format/json/api_key/{api_key}/component/{component}'
|
||||||
|
|
||||||
#media-monitor
|
#media-monitor
|
||||||
api_config['media_setup_url'] = 'media-monitor-setup/format/json/api_key/%%api_key%%'
|
api_endpoints['media_setup_url'] = 'media-monitor-setup/format/json/api_key/{api_key}'
|
||||||
api_config['upload_recorded'] = 'upload-recorded/format/json/api_key/%%api_key%%/fileid/%%fileid%%/showinstanceid/%%showinstanceid%%'
|
api_endpoints['upload_recorded'] = 'upload-recorded/format/json/api_key/{api_key}/fileid/{fileid}/showinstanceid/{showinstanceid}'
|
||||||
api_config['update_media_url'] = 'reload-metadata/format/json/api_key/%%api_key%%/mode/%%mode%%'
|
api_endpoints['update_media_url'] = 'reload-metadata/format/json/api_key/{api_key}/mode/{mode}'
|
||||||
api_config['list_all_db_files'] = 'list-all-files/format/json/api_key/%%api_key%%/dir_id/%%dir_id%%/all/%%all%%'
|
api_endpoints['list_all_db_files'] = 'list-all-files/format/json/api_key/{api_key}/dir_id/{dir_id}/all/{all}'
|
||||||
api_config['list_all_watched_dirs'] = 'list-all-watched-dirs/format/json/api_key/%%api_key%%'
|
api_endpoints['list_all_watched_dirs'] = 'list-all-watched-dirs/format/json/api_key/{api_key}'
|
||||||
api_config['add_watched_dir'] = 'add-watched-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
api_endpoints['add_watched_dir'] = 'add-watched-dir/format/json/api_key/{api_key}/path/{path}'
|
||||||
api_config['remove_watched_dir'] = 'remove-watched-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
api_endpoints['remove_watched_dir'] = 'remove-watched-dir/format/json/api_key/{api_key}/path/{path}'
|
||||||
api_config['set_storage_dir'] = 'set-storage-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
api_endpoints['set_storage_dir'] = 'set-storage-dir/format/json/api_key/{api_key}/path/{path}'
|
||||||
api_config['update_fs_mount'] = 'update-file-system-mount/format/json/api_key/%%api_key%%'
|
api_endpoints['update_fs_mount'] = 'update-file-system-mount/format/json/api_key/{api_key}'
|
||||||
api_config['reload_metadata_group'] = 'reload-metadata-group/format/json/api_key/%%api_key%%'
|
api_endpoints['reload_metadata_group'] = 'reload-metadata-group/format/json/api_key/{api_key}'
|
||||||
api_config['handle_watched_dir_missing'] = 'handle-watched-dir-missing/format/json/api_key/%%api_key%%/dir/%%dir%%'
|
api_endpoints['handle_watched_dir_missing'] = 'handle-watched-dir-missing/format/json/api_key/{api_key}/dir/{dir}'
|
||||||
#show-recorder
|
#show-recorder
|
||||||
api_config['show_schedule_url'] = 'recorded-shows/format/json/api_key/%%api_key%%'
|
api_endpoints['show_schedule_url'] = 'recorded-shows/format/json/api_key/{api_key}'
|
||||||
api_config['upload_file_url'] = 'rest/media'
|
api_endpoints['upload_file_url'] = 'rest/media'
|
||||||
api_config['upload_retries'] = '3'
|
api_endpoints['upload_retries'] = '3'
|
||||||
api_config['upload_wait'] = '60'
|
api_endpoints['upload_wait'] = '60'
|
||||||
#pypo
|
#pypo
|
||||||
api_config['export_url'] = 'schedule/api_key/%%api_key%%'
|
api_endpoints['export_url'] = 'schedule/api_key/{api_key}'
|
||||||
api_config['get_media_url'] = 'get-media/file/%%file%%/api_key/%%api_key%%'
|
api_endpoints['get_media_url'] = 'get-media/file/{file}/api_key/{api_key}'
|
||||||
api_config['update_item_url'] = 'notify-schedule-group-play/api_key/%%api_key%%/schedule_id/%%schedule_id%%'
|
api_endpoints['update_item_url'] = 'notify-schedule-group-play/api_key/{api_key}/schedule_id/{schedule_id}'
|
||||||
api_config['update_start_playing_url'] = 'notify-media-item-start-play/api_key/%%api_key%%/media_id/%%media_id%%/'
|
api_endpoints['update_start_playing_url'] = 'notify-media-item-start-play/api_key/{api_key}/media_id/{media_id}/'
|
||||||
api_config['get_stream_setting'] = 'get-stream-setting/format/json/api_key/%%api_key%%/'
|
api_endpoints['get_stream_setting'] = 'get-stream-setting/format/json/api_key/{api_key}/'
|
||||||
api_config['update_liquidsoap_status'] = 'update-liquidsoap-status/format/json/api_key/%%api_key%%/msg/%%msg%%/stream_id/%%stream_id%%/boot_time/%%boot_time%%'
|
api_endpoints['update_liquidsoap_status'] = 'update-liquidsoap-status/format/json/api_key/{api_key}/msg/{msg}/stream_id/{stream_id}/boot_time/{boot_time}'
|
||||||
api_config['update_source_status'] = 'update-source-status/format/json/api_key/%%api_key%%/sourcename/%%sourcename%%/status/%%status%%'
|
api_endpoints['update_source_status'] = 'update-source-status/format/json/api_key/{api_key}/sourcename/{sourcename}/status/{status}'
|
||||||
api_config['check_live_stream_auth'] = 'check-live-stream-auth/format/json/api_key/%%api_key%%/username/%%username%%/password/%%password%%/djtype/%%djtype%%'
|
api_endpoints['check_live_stream_auth'] = 'check-live-stream-auth/format/json/api_key/{api_key}/username/{username}/password/{password}/djtype/{djtype}'
|
||||||
api_config['get_bootstrap_info'] = 'get-bootstrap-info/format/json/api_key/%%api_key%%'
|
api_endpoints['get_bootstrap_info'] = 'get-bootstrap-info/format/json/api_key/{api_key}'
|
||||||
api_config['get_files_without_replay_gain'] = 'get-files-without-replay-gain/api_key/%%api_key%%/dir_id/%%dir_id%%'
|
api_endpoints['get_files_without_replay_gain'] = 'get-files-without-replay-gain/api_key/{api_key}/dir_id/{dir_id}'
|
||||||
api_config['update_replay_gain_value'] = 'update-replay-gain-value/format/json/api_key/%%api_key%%'
|
api_endpoints['update_replay_gain_value'] = 'update-replay-gain-value/format/json/api_key/{api_key}'
|
||||||
api_config['notify_webstream_data'] = 'notify-webstream-data/api_key/%%api_key%%/media_id/%%media_id%%/format/json'
|
api_endpoints['notify_webstream_data'] = 'notify-webstream-data/api_key/{api_key}/media_id/{media_id}/format/json'
|
||||||
api_config['notify_liquidsoap_started'] = 'rabbitmq-do-push/api_key/%%api_key%%/format/json'
|
api_endpoints['notify_liquidsoap_started'] = 'rabbitmq-do-push/api_key/{api_key}/format/json'
|
||||||
api_config['get_stream_parameters'] = 'get-stream-parameters/api_key/%%api_key%%/format/json'
|
api_endpoints['get_stream_parameters'] = 'get-stream-parameters/api_key/{api_key}/format/json'
|
||||||
api_config['push_stream_stats'] = 'push-stream-stats/api_key/%%api_key%%/format/json'
|
api_endpoints['push_stream_stats'] = 'push-stream-stats/api_key/{api_key}/format/json'
|
||||||
api_config['update_stream_setting_table'] = 'update-stream-setting-table/api_key/%%api_key%%/format/json'
|
api_endpoints['update_stream_setting_table'] = 'update-stream-setting-table/api_key/{api_key}/format/json'
|
||||||
api_config['get_files_without_silan_value'] = 'get-files-without-silan-value/api_key/%%api_key%%'
|
api_endpoints['get_files_without_silan_value'] = 'get-files-without-silan-value/api_key/{api_key}'
|
||||||
api_config['update_cue_values_by_silan'] = 'update-cue-values-by-silan/api_key/%%api_key%%'
|
api_endpoints['update_cue_values_by_silan'] = 'update-cue-values-by-silan/api_key/{api_key}'
|
||||||
|
api_endpoints['update_metadata_on_tunein'] = 'update-metadata-on-tunein/api_key/{api_key}'
|
||||||
api_config['api_base'] = 'api'
|
api_config['api_base'] = 'api'
|
||||||
api_config['bin_dir'] = '/usr/lib/airtime/api_clients/'
|
api_config['bin_dir'] = '/usr/lib/airtime/api_clients/'
|
||||||
api_config['update_metadata_on_tunein'] = 'update-metadata-on-tunein/api_key/%%api_key%%'
|
|
||||||
|
|
||||||
def get_protocol(config):
|
|
||||||
positive_values = ['Yes', 'yes', 'True', 'true', True]
|
|
||||||
port = config['general'].get('base_port', 80)
|
|
||||||
force_ssl = config['general'].get('force_ssl', False)
|
|
||||||
if force_ssl in positive_values:
|
|
||||||
protocol = 'https'
|
|
||||||
else:
|
|
||||||
protocol = config['general'].get('protocol')
|
|
||||||
if not protocol:
|
|
||||||
protocol = str(("http", "https")[int(port) == 443])
|
|
||||||
return protocol
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Airtime API Client
|
# Airtime API Version 1 Client
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
class UrlException(Exception): pass
|
|
||||||
|
|
||||||
class IncompleteUrl(UrlException):
|
|
||||||
def __init__(self, url): self.url = url
|
|
||||||
def __str__(self): return "Incomplete url: '%s'" % self.url
|
|
||||||
|
|
||||||
class UrlBadParam(UrlException):
|
|
||||||
def __init__(self, url, param):
|
|
||||||
self.url = url
|
|
||||||
self.param = param
|
|
||||||
def __str__(self):
|
|
||||||
return "Bad param '%s' passed into url: '%s'" % (self.param, self.url)
|
|
||||||
|
|
||||||
class ApcUrl(object):
|
|
||||||
""" A safe abstraction and testable for filling in parameters in
|
|
||||||
api_client.cfg"""
|
|
||||||
def __init__(self, base_url): self.base_url = base_url
|
|
||||||
|
|
||||||
def params(self, **params):
|
|
||||||
temp_url = self.base_url
|
|
||||||
for k, v in params.items():
|
|
||||||
wrapped_param = "%%" + k + "%%"
|
|
||||||
if wrapped_param in temp_url:
|
|
||||||
temp_url = temp_url.replace(wrapped_param, str(v))
|
|
||||||
else: raise UrlBadParam(self.base_url, k)
|
|
||||||
return ApcUrl(temp_url)
|
|
||||||
|
|
||||||
def url(self):
|
|
||||||
if '%%' in self.base_url: raise IncompleteUrl(self.base_url)
|
|
||||||
else: return self.base_url
|
|
||||||
|
|
||||||
class ApiRequest(object):
|
|
||||||
|
|
||||||
API_HTTP_REQUEST_TIMEOUT = 30 # 30 second HTTP request timeout
|
|
||||||
|
|
||||||
def __init__(self, name, url, logger=None):
|
|
||||||
self.name = name
|
|
||||||
self.url = url
|
|
||||||
self.__req = None
|
|
||||||
if logger is None: self.logger = logging
|
|
||||||
else: self.logger = logger
|
|
||||||
|
|
||||||
def __call__(self,_post_data=None, **kwargs):
|
|
||||||
final_url = self.url.params(**kwargs).url()
|
|
||||||
if _post_data is not None:
|
|
||||||
_post_data = urllib.parse.urlencode(_post_data).encode('utf-8')
|
|
||||||
self.logger.debug(final_url)
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(final_url, _post_data)
|
|
||||||
f = urllib.request.urlopen(req, timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT)
|
|
||||||
content_type = f.info().get_content_type()
|
|
||||||
response = f.read()
|
|
||||||
#Everything that calls an ApiRequest should be catching URLError explicitly
|
|
||||||
#(according to the other comments in this file and a cursory grep through the code)
|
|
||||||
#Note that URLError can occur for timeouts as well as socket.timeout
|
|
||||||
except socket.timeout:
|
|
||||||
self.logger.error('HTTP request to %s timed out', final_url)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
#self.logger.exception(e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
try:
|
|
||||||
if content_type == 'application/json':
|
|
||||||
try:
|
|
||||||
response = response.decode()
|
|
||||||
except (UnicodeDecodeError, AttributeError):
|
|
||||||
pass
|
|
||||||
data = json.loads(response)
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
raise InvalidContentType()
|
|
||||||
except Exception:
|
|
||||||
#self.logger.exception(e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def req(self, *args, **kwargs):
|
|
||||||
self.__req = lambda : self(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def retry(self, n, delay=5):
|
|
||||||
"""Try to send request n times. If after n times it fails then
|
|
||||||
we finally raise exception"""
|
|
||||||
for i in range(0,n-1):
|
|
||||||
try: return self.__req()
|
|
||||||
except Exception: time.sleep(delay)
|
|
||||||
return self.__req()
|
|
||||||
|
|
||||||
class RequestProvider(object):
|
|
||||||
""" Creates the available ApiRequest instance that can be read from
|
|
||||||
a config file """
|
|
||||||
def __init__(self, cfg):
|
|
||||||
self.config = cfg
|
|
||||||
self.requests = {}
|
|
||||||
if self.config["general"]["base_dir"].startswith("/"):
|
|
||||||
self.config["general"]["base_dir"] = self.config["general"]["base_dir"][1:]
|
|
||||||
protocol = get_protocol(self.config)
|
|
||||||
|
|
||||||
self.url = ApcUrl("%s://%s:%s/%s%s/%s" \
|
|
||||||
% (protocol, self.config["general"]["base_url"],
|
|
||||||
str(self.config["general"]["base_port"]),
|
|
||||||
self.config["general"]["base_dir"], self.config["api_base"],
|
|
||||||
'%%action%%'))
|
|
||||||
# Now we must discover the possible actions
|
|
||||||
actions = dict( (k,v) for k,v in cfg.items() if '%%api_key%%' in v)
|
|
||||||
for action_name, action_value in actions.items():
|
|
||||||
new_url = self.url.params(action=action_value).params(
|
|
||||||
api_key=self.config["general"]['api_key'])
|
|
||||||
self.requests[action_name] = ApiRequest(action_name, new_url)
|
|
||||||
|
|
||||||
def available_requests(self) : return list(self.requests.keys())
|
|
||||||
def __contains__(self, request) : return request in self.requests
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
if attr in self:
|
|
||||||
return self.requests[attr]
|
|
||||||
else:
|
|
||||||
return super(RequestProvider, self).__getattribute__(attr)
|
|
||||||
|
|
||||||
|
|
||||||
class AirtimeApiClient(object):
|
class AirtimeApiClient(object):
|
||||||
def __init__(self, logger=None,config_path='/etc/airtime/airtime.conf'):
|
def __init__(self, logger=None,config_path='/etc/airtime/airtime.conf'):
|
||||||
if logger is None: self.logger = logging
|
if logger is None: self.logger = logging
|
||||||
|
@ -213,7 +82,7 @@ class AirtimeApiClient(object):
|
||||||
try:
|
try:
|
||||||
self.config = ConfigObj(config_path)
|
self.config = ConfigObj(config_path)
|
||||||
self.config.update(api_config)
|
self.config.update(api_config)
|
||||||
self.services = RequestProvider(self.config)
|
self.services = RequestProvider(self.config, api_endpoints)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception('Error loading config file: %s', config_path)
|
self.logger.exception('Error loading config file: %s', config_path)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -223,8 +92,11 @@ class AirtimeApiClient(object):
|
||||||
except Exception: return -1
|
except Exception: return -1
|
||||||
|
|
||||||
def __get_api_version(self):
|
def __get_api_version(self):
|
||||||
try: return self.services.version_url()['api_version']
|
try:
|
||||||
except Exception: return -1
|
return self.services.version_url()['api_version']
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception(e)
|
||||||
|
return -1
|
||||||
|
|
||||||
def is_server_compatible(self, verbose=True):
|
def is_server_compatible(self, verbose=True):
|
||||||
logger = self.logger
|
logger = self.logger
|
|
@ -0,0 +1,124 @@
|
||||||
|
###############################################################################
|
||||||
|
# This file holds the implementations for all the API clients.
|
||||||
|
#
|
||||||
|
# If you want to develop a new client, here are some suggestions: Get the fetch
|
||||||
|
# methods working first, then the push, then the liquidsoap notifier. You will
|
||||||
|
# probably want to create a script on your server side to automatically
|
||||||
|
# schedule a playlist one minute from the current time.
|
||||||
|
###############################################################################
|
||||||
|
import datetime
|
||||||
|
from dateutil.parser import isoparse
|
||||||
|
import logging
|
||||||
|
from configobj import ConfigObj
|
||||||
|
from .utils import RequestProvider, time_in_seconds, time_in_milliseconds
|
||||||
|
|
||||||
|
LIBRETIME_API_VERSION = "2.0"
|
||||||
|
|
||||||
|
api_config = {}
|
||||||
|
api_endpoints = {}
|
||||||
|
|
||||||
|
api_endpoints['version_url'] = 'version/'
|
||||||
|
api_endpoints['schedule_url'] = 'schedule/'
|
||||||
|
api_endpoints['webstream_url'] = 'webstreams/{id}/'
|
||||||
|
api_endpoints['show_instance_url'] = 'show-instances/{id}/'
|
||||||
|
api_endpoints['show_url'] = 'shows/{id}/'
|
||||||
|
api_endpoints['file_url'] = 'files/{id}/'
|
||||||
|
api_endpoints['file_download_url'] = 'files/{id}/download/'
|
||||||
|
api_config['api_base'] = 'api/v2'
|
||||||
|
|
||||||
|
class AirtimeApiClient:
|
||||||
|
def __init__(self, logger=None, config_path='/etc/airtime/airtime.conf'):
|
||||||
|
if logger is None:
|
||||||
|
self.logger = logging
|
||||||
|
else:
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.config = ConfigObj(config_path)
|
||||||
|
self.config.update(api_config)
|
||||||
|
self.services = RequestProvider(self.config, api_endpoints)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception('Error loading config file: %s', config_path)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_schedule(self):
|
||||||
|
current_time = datetime.datetime.utcnow()
|
||||||
|
end_time = current_time + datetime.timedelta(hours=1)
|
||||||
|
|
||||||
|
str_current = current_time.isoformat(timespec='seconds')
|
||||||
|
str_end = end_time.isoformat(timespec='seconds')
|
||||||
|
data = self.services.schedule_url(params={
|
||||||
|
'ends__range': (f'{str_current}Z,{str_end}Z'),
|
||||||
|
})
|
||||||
|
result = {'media': {} }
|
||||||
|
for item in data:
|
||||||
|
start = isoparse(item['starts'])
|
||||||
|
key = start.strftime('%YYYY-%mm-%dd-%HH-%MM-%SS')
|
||||||
|
end = isoparse(item['ends'])
|
||||||
|
|
||||||
|
show_instance = self.services.show_instance_url(id=item['instance_id'])
|
||||||
|
show = self.services.show_url(id=show_instance['show_id'])
|
||||||
|
|
||||||
|
result['media'][key] = {
|
||||||
|
'start': start.strftime('%Y-%m-%d-%H-%M-%S'),
|
||||||
|
'end': end.strftime('%Y-%m-%d-%H-%M-%S'),
|
||||||
|
'row_id': item['id']
|
||||||
|
}
|
||||||
|
current = result['media'][key]
|
||||||
|
if item['file']:
|
||||||
|
current['independent_event'] = False
|
||||||
|
current['type'] = 'file'
|
||||||
|
current['id'] = item['file_id']
|
||||||
|
|
||||||
|
fade_in = time_in_milliseconds(datetime.time.fromisoformat(item['fade_in']))
|
||||||
|
fade_out = time_in_milliseconds(datetime.time.fromisoformat(item['fade_out']))
|
||||||
|
|
||||||
|
cue_in = time_in_seconds(datetime.time.fromisoformat(item['cue_in']))
|
||||||
|
cue_out = time_in_seconds(datetime.time.fromisoformat(item['cue_out']))
|
||||||
|
|
||||||
|
current['fade_in'] = fade_in
|
||||||
|
current['fade_out'] = fade_out
|
||||||
|
current['cue_in'] = cue_in
|
||||||
|
current['cue_out'] = cue_out
|
||||||
|
|
||||||
|
info = self.services.file_url(id=item['file_id'])
|
||||||
|
current['metadata'] = info
|
||||||
|
current['uri'] = item['file']
|
||||||
|
current['filesize'] = info['filesize']
|
||||||
|
elif item['stream']:
|
||||||
|
current['independent_event'] = True
|
||||||
|
current['id'] = item['stream_id']
|
||||||
|
info = self.services.webstream_url(id=item['stream_id'])
|
||||||
|
current['uri'] = info['url']
|
||||||
|
current['type'] = 'stream_buffer_start'
|
||||||
|
# Stream events are instantaneous
|
||||||
|
current['end'] = current['start']
|
||||||
|
|
||||||
|
result[f'{key}_0'] = {
|
||||||
|
'id': current['id'],
|
||||||
|
'type': 'stream_output_start',
|
||||||
|
'start': current['start'],
|
||||||
|
'end': current['start'],
|
||||||
|
'uri': current['uri'],
|
||||||
|
'row_id': current['row_id'],
|
||||||
|
'independent_event': current['independent_event'],
|
||||||
|
}
|
||||||
|
|
||||||
|
result[end.isoformat()] = {
|
||||||
|
'type': 'stream_buffer_end',
|
||||||
|
'start': current['end'],
|
||||||
|
'end': current['end'],
|
||||||
|
'uri': current['uri'],
|
||||||
|
'row_id': current['row_id'],
|
||||||
|
'independent_event': current['independent_event'],
|
||||||
|
}
|
||||||
|
|
||||||
|
result[f'{end.isoformat()}_0'] = {
|
||||||
|
'type': 'stream_output_end',
|
||||||
|
'start': current['end'],
|
||||||
|
'end': current['end'],
|
||||||
|
'uri': current['uri'],
|
||||||
|
'row_id': current['row_id'],
|
||||||
|
'independent_event': current['independent_event'],
|
||||||
|
}
|
||||||
|
return result
|
|
@ -9,15 +9,16 @@ print(script_path)
|
||||||
os.chdir(script_path)
|
os.chdir(script_path)
|
||||||
|
|
||||||
setup(name='api_clients',
|
setup(name='api_clients',
|
||||||
version='1.0',
|
version='2.0.0',
|
||||||
description='Airtime API Client',
|
description='LibreTime API Client',
|
||||||
url='http://github.com/sourcefabric/Airtime',
|
url='http://github.com/LibreTime/Libretime',
|
||||||
author='sourcefabric',
|
author='LibreTime Contributors',
|
||||||
license='AGPLv3',
|
license='AGPLv3',
|
||||||
packages=['api_clients'],
|
packages=['api_clients'],
|
||||||
scripts=[],
|
scripts=[],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'configobj'
|
'configobj',
|
||||||
|
'python-dateutil',
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
data_files=[])
|
data_files=[])
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import unittest
|
import unittest
|
||||||
from api_clients.api_client import ApcUrl, UrlBadParam, IncompleteUrl
|
from api_clients.utils import ApcUrl, UrlBadParam, IncompleteUrl
|
||||||
|
|
||||||
class TestApcUrl(unittest.TestCase):
|
class TestApcUrl(unittest.TestCase):
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
|
@ -8,16 +8,16 @@ class TestApcUrl(unittest.TestCase):
|
||||||
self.assertEqual(u.base_url, url)
|
self.assertEqual(u.base_url, url)
|
||||||
|
|
||||||
def test_params_1(self):
|
def test_params_1(self):
|
||||||
u = ApcUrl("/testing/%%key%%")
|
u = ApcUrl("/testing/{key}")
|
||||||
self.assertEqual(u.params(key='val').url(), '/testing/val')
|
self.assertEqual(u.params(key='val').url(), '/testing/val')
|
||||||
|
|
||||||
def test_params_2(self):
|
def test_params_2(self):
|
||||||
u = ApcUrl('/testing/%%key%%/%%api%%/more_testing')
|
u = ApcUrl('/testing/{key}/{api}/more_testing')
|
||||||
full_url = u.params(key="AAA",api="BBB").url()
|
full_url = u.params(key="AAA",api="BBB").url()
|
||||||
self.assertEqual(full_url, '/testing/AAA/BBB/more_testing')
|
self.assertEqual(full_url, '/testing/AAA/BBB/more_testing')
|
||||||
|
|
||||||
def test_params_ex(self):
|
def test_params_ex(self):
|
||||||
u = ApcUrl("/testing/%%key%%")
|
u = ApcUrl("/testing/{key}")
|
||||||
with self.assertRaises(UrlBadParam):
|
with self.assertRaises(UrlBadParam):
|
||||||
u.params(bad_key='testing')
|
u.params(bad_key='testing')
|
||||||
|
|
||||||
|
@ -26,5 +26,5 @@ class TestApcUrl(unittest.TestCase):
|
||||||
self.assertEqual( ApcUrl(u).url(), u )
|
self.assertEqual( ApcUrl(u).url(), u )
|
||||||
|
|
||||||
def test_url_ex(self):
|
def test_url_ex(self):
|
||||||
u = ApcUrl('/%%one%%/%%two%%/three').params(two='testing')
|
u = ApcUrl('/{one}/{two}/three').params(two='testing')
|
||||||
with self.assertRaises(IncompleteUrl): u.url()
|
with self.assertRaises(IncompleteUrl): u.url()
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
import unittest
|
import unittest
|
||||||
import json
|
import json
|
||||||
from mock import MagicMock, patch
|
from mock import MagicMock, patch
|
||||||
from api_clients.api_client import ApcUrl, ApiRequest
|
from api_clients.utils import ApcUrl, ApiRequest
|
||||||
|
|
||||||
class ResponseInfo:
|
class ResponseInfo:
|
||||||
def get_content_type(self):
|
@property
|
||||||
return 'application/json'
|
def headers(self):
|
||||||
|
return {'content-type': 'application/json'}
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {'ok', 'ok'}
|
||||||
|
|
||||||
class TestApiRequest(unittest.TestCase):
|
class TestApiRequest(unittest.TestCase):
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
u = ApiRequest('request_name', ApcUrl('/test/ing'))
|
u = ApiRequest('request_name', ApcUrl('/test/ing'))
|
||||||
self.assertEqual(u.name, "request_name")
|
self.assertEqual(u.name, "request_name")
|
||||||
|
|
||||||
def test_call(self):
|
def test_call_json(self):
|
||||||
ret = json.dumps( {'ok':'ok'} )
|
ret = {'ok':'ok'}
|
||||||
read = MagicMock()
|
read = MagicMock()
|
||||||
read.read = MagicMock(return_value=ret)
|
read.headers = {'content-type': 'application/json'}
|
||||||
read.info = MagicMock(return_value=ResponseInfo())
|
read.json = MagicMock(return_value=ret)
|
||||||
u = 'http://localhost/testing'
|
u = 'http://localhost/testing'
|
||||||
with patch('urllib.request.urlopen') as mock_method:
|
with patch('requests.get') as mock_method:
|
||||||
mock_method.return_value = read
|
mock_method.return_value = read
|
||||||
request = ApiRequest('mm', ApcUrl(u))()
|
request = ApiRequest('mm', ApcUrl(u))()
|
||||||
self.assertEqual(request, json.loads(ret))
|
self.assertEqual(request, ret)
|
||||||
|
|
||||||
|
def test_call_html(self):
|
||||||
|
ret = '<html><head></head><body></body></html>'
|
||||||
|
read = MagicMock()
|
||||||
|
read.headers = {'content-type': 'application/html'}
|
||||||
|
read.text = MagicMock(return_value=ret)
|
||||||
|
u = 'http://localhost/testing'
|
||||||
|
with patch('requests.get') as mock_method:
|
||||||
|
mock_method.return_value = read
|
||||||
|
request = ApiRequest('mm', ApcUrl(u))()
|
||||||
|
self.assertEqual(request.text(), ret)
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
if __name__ == '__main__': unittest.main()
|
||||||
|
|
|
@ -2,7 +2,8 @@ import unittest
|
||||||
import json
|
import json
|
||||||
from mock import patch, MagicMock
|
from mock import patch, MagicMock
|
||||||
from configobj import ConfigObj
|
from configobj import ConfigObj
|
||||||
from api_clients.api_client import RequestProvider, api_config
|
from api_clients.version1 import api_config
|
||||||
|
from api_clients.utils import RequestProvider
|
||||||
|
|
||||||
class TestRequestProvider(unittest.TestCase):
|
class TestRequestProvider(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -18,13 +19,17 @@ class TestRequestProvider(unittest.TestCase):
|
||||||
self.assertTrue('general' in self.cfg)
|
self.assertTrue('general' in self.cfg)
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
rp = RequestProvider(self.cfg)
|
rp = RequestProvider(self.cfg, {})
|
||||||
self.assertTrue( len( rp.available_requests() ) > 0 )
|
self.assertEqual(len(rp.available_requests()), 0)
|
||||||
|
|
||||||
def test_contains(self):
|
def test_contains(self):
|
||||||
rp = RequestProvider(self.cfg)
|
methods = {
|
||||||
methods = ['upload_recorded', 'update_media_url', 'list_all_db_files']
|
'upload_recorded': '/1/',
|
||||||
|
'update_media_url': '/2/',
|
||||||
|
'list_all_db_files': '/3/',
|
||||||
|
}
|
||||||
|
rp = RequestProvider(self.cfg, methods)
|
||||||
for meth in methods:
|
for meth in methods:
|
||||||
self.assertTrue( meth in rp.requests )
|
self.assertTrue(meth in rp.requests)
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
if __name__ == '__main__': unittest.main()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import unittest
|
import datetime
|
||||||
import configparser
|
import configparser
|
||||||
from api_clients.api_client import get_protocol
|
import unittest
|
||||||
|
from api_clients import utils
|
||||||
|
|
||||||
def get_force_ssl(value, useConfigParser):
|
def get_force_ssl(value, useConfigParser):
|
||||||
config = {}
|
config = {}
|
||||||
|
@ -10,12 +11,23 @@ def get_force_ssl(value, useConfigParser):
|
||||||
'base_port': 80,
|
'base_port': 80,
|
||||||
'force_ssl': value,
|
'force_ssl': value,
|
||||||
}
|
}
|
||||||
return get_protocol(config)
|
return utils.get_protocol(config)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTime(unittest.TestCase):
|
||||||
|
def test_time_in_seconds(self):
|
||||||
|
time = datetime.time(hour=0, minute=3, second=34, microsecond=649600)
|
||||||
|
self.assertTrue(abs(utils.time_in_seconds(time) - 214.65) < 0.009)
|
||||||
|
|
||||||
|
def test_time_in_milliseconds(self):
|
||||||
|
time = datetime.time(hour=0, minute=0, second=0, microsecond=500000)
|
||||||
|
self.assertEqual(utils.time_in_milliseconds(time), 500)
|
||||||
|
|
||||||
|
|
||||||
class TestGetProtocol(unittest.TestCase):
|
class TestGetProtocol(unittest.TestCase):
|
||||||
def test_dict_config_empty_http(self):
|
def test_dict_config_empty_http(self):
|
||||||
config = {'general': {}}
|
config = {'general': {}}
|
||||||
protocol = get_protocol(config)
|
protocol = utils.get_protocol(config)
|
||||||
self.assertEqual(protocol, 'http')
|
self.assertEqual(protocol, 'http')
|
||||||
|
|
||||||
def test_dict_config_http(self):
|
def test_dict_config_http(self):
|
||||||
|
@ -24,7 +36,7 @@ class TestGetProtocol(unittest.TestCase):
|
||||||
'base_port': 80,
|
'base_port': 80,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
protocol = get_protocol(config)
|
protocol = utils.get_protocol(config)
|
||||||
self.assertEqual(protocol, 'http')
|
self.assertEqual(protocol, 'http')
|
||||||
|
|
||||||
def test_dict_config_https(self):
|
def test_dict_config_https(self):
|
||||||
|
@ -33,7 +45,7 @@ class TestGetProtocol(unittest.TestCase):
|
||||||
'base_port': 443,
|
'base_port': 443,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
protocol = get_protocol(config)
|
protocol = utils.get_protocol(config)
|
||||||
self.assertEqual(protocol, 'https')
|
self.assertEqual(protocol, 'https')
|
||||||
|
|
||||||
def test_dict_config_force_https(self):
|
def test_dict_config_force_https(self):
|
||||||
|
@ -47,7 +59,7 @@ class TestGetProtocol(unittest.TestCase):
|
||||||
def test_configparser_config_empty_http(self):
|
def test_configparser_config_empty_http(self):
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config['general'] = {}
|
config['general'] = {}
|
||||||
protocol = get_protocol(config)
|
protocol = utils.get_protocol(config)
|
||||||
self.assertEqual(protocol, 'http')
|
self.assertEqual(protocol, 'http')
|
||||||
|
|
||||||
def test_configparser_config_http(self):
|
def test_configparser_config_http(self):
|
||||||
|
@ -55,7 +67,7 @@ class TestGetProtocol(unittest.TestCase):
|
||||||
config['general'] = {
|
config['general'] = {
|
||||||
'base_port': 80,
|
'base_port': 80,
|
||||||
}
|
}
|
||||||
protocol = get_protocol(config)
|
protocol = utils.get_protocol(config)
|
||||||
self.assertEqual(protocol, 'http')
|
self.assertEqual(protocol, 'http')
|
||||||
|
|
||||||
def test_configparser_config_https(self):
|
def test_configparser_config_https(self):
|
||||||
|
@ -63,7 +75,7 @@ class TestGetProtocol(unittest.TestCase):
|
||||||
config['general'] = {
|
config['general'] = {
|
||||||
'base_port': 443,
|
'base_port': 443,
|
||||||
}
|
}
|
||||||
protocol = get_protocol(config)
|
protocol = utils.get_protocol(config)
|
||||||
self.assertEqual(protocol, 'https')
|
self.assertEqual(protocol, 'https')
|
||||||
|
|
||||||
def test_configparser_config_force_https(self):
|
def test_configparser_config_force_https(self):
|
||||||
|
@ -73,3 +85,5 @@ class TestGetProtocol(unittest.TestCase):
|
||||||
self.assertEqual(get_force_ssl(value, True), 'https')
|
self.assertEqual(get_force_ssl(value, True), 'https')
|
||||||
for value in negative_values:
|
for value in negative_values:
|
||||||
self.assertEqual(get_force_ssl(value, True), 'http')
|
self.assertEqual(get_force_ssl(value, True), 'http')
|
||||||
|
|
||||||
|
if __name__ == '__main__': unittest.main()
|
|
@ -28,7 +28,7 @@ from configobj import ConfigObj
|
||||||
|
|
||||||
# custom imports
|
# custom imports
|
||||||
#from util import *
|
#from util import *
|
||||||
from api_clients import *
|
from api_clients import version1 as api_client
|
||||||
|
|
||||||
LOG_LEVEL = logging.INFO
|
LOG_LEVEL = logging.INFO
|
||||||
LOG_PATH = '/var/log/airtime/pypo/notify.log'
|
LOG_PATH = '/var/log/airtime/pypo/notify.log'
|
||||||
|
|
|
@ -4,7 +4,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from api_clients.api_client import AirtimeApiClient
|
from api_clients.version1 import AirtimeApiClient
|
||||||
|
|
||||||
def generate_liquidsoap_config(ss):
|
def generate_liquidsoap_config(ss):
|
||||||
data = ss['msg']
|
data = ss['msg']
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
from api_clients import *
|
from api_clients import version1 as api_client
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
api_clients = api_client.AirtimeApiClient()
|
api_clients = api_client.AirtimeApiClient()
|
||||||
|
|
|
@ -12,7 +12,7 @@ import sys
|
||||||
import telnetlib
|
import telnetlib
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from api_clients import api_client
|
from api_clients import version1 as api_client
|
||||||
from configobj import ConfigObj
|
from configobj import ConfigObj
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
|
@ -7,7 +7,7 @@ import traceback
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from api_clients import api_client
|
from api_clients import version1 as api_client
|
||||||
|
|
||||||
class ListenerStat(Thread):
|
class ListenerStat(Thread):
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ from queue import Empty
|
||||||
from threading import Thread, Timer
|
from threading import Thread, Timer
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
from api_clients import api_client
|
from api_clients import version1 as v1_api_client
|
||||||
|
from api_clients import version2 as api_client
|
||||||
from .timeout import ls_timeout
|
from .timeout import ls_timeout
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ signal.signal(signal.SIGINT, keyboardInterruptHandler)
|
||||||
|
|
||||||
logging.captureWarnings(True)
|
logging.captureWarnings(True)
|
||||||
|
|
||||||
POLL_INTERVAL = 480
|
POLL_INTERVAL = 400
|
||||||
|
|
||||||
class PypoFetch(Thread):
|
class PypoFetch(Thread):
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ class PypoFetch(Thread):
|
||||||
#Hacky...
|
#Hacky...
|
||||||
PypoFetch.ref = self
|
PypoFetch.ref = self
|
||||||
|
|
||||||
|
self.v1_api_client = v1_api_client.AirtimeApiClient()
|
||||||
self.api_client = api_client.AirtimeApiClient()
|
self.api_client = api_client.AirtimeApiClient()
|
||||||
self.fetch_queue = pypoFetch_q
|
self.fetch_queue = pypoFetch_q
|
||||||
self.push_queue = pypoPush_q
|
self.push_queue = pypoPush_q
|
||||||
|
@ -150,7 +152,7 @@ class PypoFetch(Thread):
|
||||||
def set_bootstrap_variables(self):
|
def set_bootstrap_variables(self):
|
||||||
self.logger.debug('Getting information needed on bootstrap from Airtime')
|
self.logger.debug('Getting information needed on bootstrap from Airtime')
|
||||||
try:
|
try:
|
||||||
info = self.api_client.get_bootstrap_info()
|
info = self.v1_api_client.get_bootstrap_info()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception('Unable to get bootstrap info.. Exiting pypo...')
|
self.logger.exception('Unable to get bootstrap info.. Exiting pypo...')
|
||||||
|
|
||||||
|
@ -255,7 +257,7 @@ class PypoFetch(Thread):
|
||||||
stream_id = info[0]
|
stream_id = info[0]
|
||||||
status = info[1]
|
status = info[1]
|
||||||
if(status == "true"):
|
if(status == "true"):
|
||||||
self.api_client.notify_liquidsoap_status("OK", stream_id, str(fake_time))
|
self.v1_api_client.notify_liquidsoap_status("OK", stream_id, str(fake_time))
|
||||||
|
|
||||||
|
|
||||||
@ls_timeout
|
@ls_timeout
|
||||||
|
@ -343,7 +345,7 @@ class PypoFetch(Thread):
|
||||||
media_item = media[key]
|
media_item = media[key]
|
||||||
if (media_item['type'] == 'file'):
|
if (media_item['type'] == 'file'):
|
||||||
fileExt = self.sanity_check_media_item(media_item)
|
fileExt = self.sanity_check_media_item(media_item)
|
||||||
dst = os.path.join(download_dir, "{}{}".format(media_item['id'], fileExt))
|
dst = os.path.join(download_dir, f'{media_item["id"]}{fileExt}')
|
||||||
media_item['dst'] = dst
|
media_item['dst'] = dst
|
||||||
media_item['file_ready'] = False
|
media_item['file_ready'] = False
|
||||||
media_filtered[key] = media_item
|
media_filtered[key] = media_item
|
||||||
|
@ -434,10 +436,14 @@ class PypoFetch(Thread):
|
||||||
self.logger.exception("Problem removing file '%s'" % f)
|
self.logger.exception("Problem removing file '%s'" % f)
|
||||||
|
|
||||||
def manual_schedule_fetch(self):
|
def manual_schedule_fetch(self):
|
||||||
success, self.schedule_data = self.api_client.get_schedule()
|
try:
|
||||||
if success:
|
self.schedule_data = self.api_client.get_schedule()
|
||||||
self.process_schedule(self.schedule_data)
|
self.process_schedule(self.schedule_data)
|
||||||
return success
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error('Unable to fetch schedule')
|
||||||
|
self.logger.exception(e)
|
||||||
|
return False
|
||||||
|
|
||||||
def persistent_manual_schedule_fetch(self, max_attempts=1):
|
def persistent_manual_schedule_fetch(self, max_attempts=1):
|
||||||
success = False
|
success = False
|
||||||
|
@ -452,7 +458,7 @@ class PypoFetch(Thread):
|
||||||
# push metadata to TuneIn. We have to do this because TuneIn turns
|
# push metadata to TuneIn. We have to do this because TuneIn turns
|
||||||
# off metadata if it does not receive a request every 5 minutes.
|
# off metadata if it does not receive a request every 5 minutes.
|
||||||
def update_metadata_on_tunein(self):
|
def update_metadata_on_tunein(self):
|
||||||
self.api_client.update_metadata_on_tunein()
|
self.v1_api_client.update_metadata_on_tunein()
|
||||||
Timer(120, self.update_metadata_on_tunein).start()
|
Timer(120, self.update_metadata_on_tunein).start()
|
||||||
|
|
||||||
def main(self):
|
def main(self):
|
||||||
|
|
|
@ -16,6 +16,7 @@ import configparser
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
from requests.exceptions import ConnectionError, HTTPError, Timeout
|
from requests.exceptions import ConnectionError, HTTPError, Timeout
|
||||||
|
from api_clients import version2 as api_client
|
||||||
|
|
||||||
CONFIG_PATH = '/etc/airtime/airtime.conf'
|
CONFIG_PATH = '/etc/airtime/airtime.conf'
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ class PypoFile(Thread):
|
||||||
self.media = None
|
self.media = None
|
||||||
self.cache_dir = os.path.join(config["cache_dir"], "scheduler")
|
self.cache_dir = os.path.join(config["cache_dir"], "scheduler")
|
||||||
self._config = self.read_config_file(CONFIG_PATH)
|
self._config = self.read_config_file(CONFIG_PATH)
|
||||||
|
self.api_client = api_client.AirtimeApiClient()
|
||||||
|
|
||||||
def copy_file(self, media_item):
|
def copy_file(self, media_item):
|
||||||
"""
|
"""
|
||||||
|
@ -44,6 +46,8 @@ class PypoFile(Thread):
|
||||||
dst_exists = True
|
dst_exists = True
|
||||||
try:
|
try:
|
||||||
dst_size = os.path.getsize(dst)
|
dst_size = os.path.getsize(dst)
|
||||||
|
if dst_size == 0:
|
||||||
|
dst_exists = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dst_exists = False
|
dst_exists = False
|
||||||
|
|
||||||
|
@ -63,41 +67,16 @@ class PypoFile(Thread):
|
||||||
|
|
||||||
if do_copy:
|
if do_copy:
|
||||||
self.logger.info("copying from %s to local cache %s" % (src, dst))
|
self.logger.info("copying from %s to local cache %s" % (src, dst))
|
||||||
|
|
||||||
CONFIG_SECTION = 'general'
|
|
||||||
username = self._config[CONFIG_SECTION].get('api_key')
|
|
||||||
baseurl = self._config[CONFIG_SECTION].get('base_url')
|
|
||||||
port = self._config[CONFIG_SECTION].get('base_port', 80)
|
|
||||||
positive_values = ['Yes', 'yes', 'True', 'true', True]
|
|
||||||
force_ssl = self._config[CONFIG_SECTION].get('force_ssl', False)
|
|
||||||
if force_ssl in positive_values:
|
|
||||||
protocol = 'https'
|
|
||||||
self.logger.debug('protocol set to https from force_ssl configuration setting')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
protocol = self._config[CONFIG_SECTION]['protocol']
|
|
||||||
self.logger.debug('protocol set to %s from configuration setting' % (protocol))
|
|
||||||
except (NoOptionError, KeyError) as e:
|
|
||||||
protocol = str(("http", "https")[int(port) == 443])
|
|
||||||
self.logger.debug('guessing protocol as %s from port configuration' % (protocol))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
host = [protocol, baseurl, port]
|
|
||||||
url = "%s://%s:%s/rest/media/%s/download" % (host[0],
|
|
||||||
host[1],
|
|
||||||
host[2],
|
|
||||||
media_item["id"])
|
|
||||||
with open(dst, "wb") as handle:
|
with open(dst, "wb") as handle:
|
||||||
response = requests.get(url, auth=requests.auth.HTTPBasicAuth(username, ''), stream=True, verify=False)
|
self.logger.info(media_item)
|
||||||
|
response = self.api_client.services.file_download_url(id=media_item['id'])
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
self.logger.error(response)
|
self.logger.error(response)
|
||||||
raise Exception("%s - Error occurred downloading file" % response.status_code)
|
raise Exception("%s - Error occurred downloading file" % response.status_code)
|
||||||
|
|
||||||
for chunk in response.iter_content(1024):
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
|
|
||||||
handle.write(chunk)
|
handle.write(chunk)
|
||||||
|
|
||||||
#make file world readable and owner writable
|
#make file world readable and owner writable
|
||||||
|
|
|
@ -20,7 +20,7 @@ from queue import Empty, Queue
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from api_clients import api_client
|
from api_clients import version1 as api_client
|
||||||
from .timeout import ls_timeout
|
from .timeout import ls_timeout
|
||||||
|
|
||||||
logging.captureWarnings(True)
|
logging.captureWarnings(True)
|
||||||
|
|
|
@ -21,14 +21,15 @@ from threading import Thread
|
||||||
|
|
||||||
import mutagen
|
import mutagen
|
||||||
|
|
||||||
from api_clients import api_client as apc
|
from api_clients import version1 as v1_api_client
|
||||||
|
from api_clients import version2 as api_client
|
||||||
|
|
||||||
def api_client(logger):
|
def api_client(logger):
|
||||||
"""
|
"""
|
||||||
api_client returns the correct instance of AirtimeApiClient. Although there is only one
|
api_client returns the correct instance of AirtimeApiClient. Although there is only one
|
||||||
instance to choose from at the moment.
|
instance to choose from at the moment.
|
||||||
"""
|
"""
|
||||||
return apc.AirtimeApiClient(logger)
|
return v1_api_client.AirtimeApiClient(logger)
|
||||||
|
|
||||||
# loading config file
|
# loading config file
|
||||||
try:
|
try:
|
||||||
|
|
Loading…
Reference in New Issue