add API v2
This commit is contained in:
parent
f809c3a8ff
commit
2df0189a90
|
@ -19,4 +19,5 @@ airtime_mvc/tests/log/*.log
|
|||
/docs/_site/*
|
||||
/docs/.jekyll-cache/*
|
||||
/docs/.gems/*
|
||||
Gemfile.lock
|
||||
Gemfile.lock
|
||||
api.log
|
||||
|
|
|
@ -10,6 +10,10 @@ Vagrant.configure("2") do |config|
|
|||
# liquidsoap input harbors for instreaming (ie. /master)
|
||||
config.vm.network "forwarded_port", guest: 8001, host:8001
|
||||
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)
|
||||
nfsPath = "."
|
||||
|
|
|
@ -18,6 +18,7 @@ $pypo = $externalServices["pypo"];
|
|||
$liquidsoap = $externalServices["liquidsoap"];
|
||||
$analyzer = $externalServices["analyzer"];
|
||||
$celery = $externalServices['celery'];
|
||||
$api = $externalServices['api'];
|
||||
|
||||
$r1 = array_reduce($phpDependencies, "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">
|
||||
</head>
|
||||
<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
|
||||
*/
|
||||
html {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
padding: 2em;
|
||||
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
|
||||
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.
|
||||
<?php
|
||||
}
|
||||
|
@ -243,6 +244,26 @@ $result = $r1 && $r2;
|
|||
?>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
$liquidsoap = $externalServices["liquidsoap"];
|
||||
$analyzer = $externalServices["analyzer"];
|
||||
$celery = $externalServices['celery'];
|
||||
$api = $externalServices['api'];
|
||||
|
||||
$r1 = array_reduce($phpDependencies, "booleanReduce", true);
|
||||
$r2 = array_reduce($externalServices, "booleanReduce", true);
|
||||
|
@ -170,6 +171,26 @@
|
|||
?>
|
||||
</td>
|
||||
</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>
|
||||
<tr id="partitions" class="even">
|
||||
<th colspan="5"><?php echo _("Disk Space") ?></th>
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
<p>
|
||||
Looks like you're almost done! As a final step, please run the following commands from the terminal:
|
||||
</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-analyzer
|
||||
sudo systemctl start libretime-celery</pre>
|
||||
sudo systemctl start libretime-playout</pre
|
||||
<p>
|
||||
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!
|
||||
|
|
|
@ -56,6 +56,7 @@ function checkExternalServices() {
|
|||
"liquidsoap" => checkLiquidsoapService(),
|
||||
"rabbitmq" => checkRMQConnection(),
|
||||
"celery" => checkCeleryService(),
|
||||
"api" => checkApiService(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -158,3 +159,16 @@ function checkCeleryService() {
|
|||
}
|
||||
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
|
||||
*/
|
||||
class DatabaseSetup extends Setup {
|
||||
|
||||
|
||||
// airtime.conf section header
|
||||
protected static $_section = "[database]";
|
||||
|
||||
|
@ -80,6 +80,7 @@ class DatabaseSetup extends Setup {
|
|||
$this->checkSchemaExists();
|
||||
$this->createDatabaseTables();
|
||||
$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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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->bindValue(':icecastpass', $icecast_pass, PDO::PARAM_STR);
|
||||
try {
|
||||
$statement->execute();
|
||||
$statement->execute();
|
||||
}
|
||||
catch (PDOException $ex) {
|
||||
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"
|
||||
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
|
||||
if [[ ! -e $source_path ]]; then
|
||||
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
|
||||
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..."
|
||||
# Make the airtime log directory group-writable
|
||||
loudCmd "chmod -R 775 /var/log/airtime"
|
||||
|
@ -1058,15 +1073,15 @@ fi
|
|||
|
||||
# Enable Apache modules
|
||||
if $is_debian_buster; then
|
||||
loudCmd "a2enmod rewrite php7.3"
|
||||
loudCmd "a2enmod rewrite php7.3 proxy proxy_http"
|
||||
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
|
||||
loudCmd "a2enmod rewrite php7.0"
|
||||
loudCmd "a2enmod rewrite php7.0 proxy proxy_http"
|
||||
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
|
||||
loudCmd "a2enmod rewrite php5"
|
||||
loudCmd "a2enmod rewrite php5 proxy proxy_http"
|
||||
fi
|
||||
|
||||
if [ $skip_postgres -eq 0 ]; then
|
||||
|
|
|
@ -1,29 +1,34 @@
|
|||
WEB_PORT_LISTEN
|
||||
|
||||
<VirtualHost *:WEB_PORT>
|
||||
ServerAdmin foo@bar.org
|
||||
DocumentRoot WEB_ROOT
|
||||
php_admin_value upload_tmp_dir /tmp
|
||||
php_value post_max_size 500M
|
||||
php_value upload_max_filesize 500M
|
||||
php_value request_order "GPC"
|
||||
php_value session.gc_probability 0
|
||||
php_value session.auto_start 0
|
||||
ServerAdmin foo@bar.org
|
||||
DocumentRoot WEB_ROOT
|
||||
php_admin_value upload_tmp_dir /tmp
|
||||
php_value post_max_size 500M
|
||||
php_value upload_max_filesize 500M
|
||||
php_value request_order "GPC"
|
||||
php_value session.gc_probability 0
|
||||
php_value session.auto_start 0
|
||||
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
AddOutputFilterByType DEFLATE application/json
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
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>
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} -s [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||
|
@ -34,6 +39,10 @@ WEB_PORT_LISTEN
|
|||
Allow from all
|
||||
|
||||
Require all granted
|
||||
|
||||
</Directory>
|
||||
|
||||
<Directory /usr/share/airtime/api>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
</VirtualHost>
|
||||
|
|
|
@ -1,26 +1,32 @@
|
|||
WEB_PORT_LISTEN
|
||||
|
||||
<VirtualHost *:WEB_PORT>
|
||||
ServerAdmin foo@bar.org
|
||||
DocumentRoot WEB_ROOT
|
||||
php_admin_value upload_tmp_dir /tmp
|
||||
php_value post_max_size 500M
|
||||
php_value upload_max_filesize 500M
|
||||
php_value request_order "GPC"
|
||||
php_value session.gc_probability 0
|
||||
php_value session.auto_start 0
|
||||
ServerAdmin foo@bar.org
|
||||
DocumentRoot WEB_ROOT
|
||||
php_admin_value upload_tmp_dir /tmp
|
||||
php_value post_max_size 500M
|
||||
php_value upload_max_filesize 500M
|
||||
php_value request_order "GPC"
|
||||
php_value session.gc_probability 0
|
||||
php_value session.auto_start 0
|
||||
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
AddOutputFilterByType DEFLATE application/json
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
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>
|
||||
RewriteEngine On
|
||||
|
@ -37,4 +43,8 @@ WEB_PORT_LISTEN
|
|||
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /usr/share/airtime/api>
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
|
|
@ -22,6 +22,7 @@ libmad-ocaml
|
|||
libopus0
|
||||
libportaudio2
|
||||
libpulse0
|
||||
libpq-dev
|
||||
libsamplerate0
|
||||
libsoundtouch-ocaml
|
||||
libtaglib-ocaml
|
||||
|
@ -58,6 +59,8 @@ rabbitmq-server
|
|||
silan
|
||||
systemd-sysv
|
||||
unzip
|
||||
uwsgi
|
||||
uwsgi-plugin-python3
|
||||
vorbisgain
|
||||
vorbis-tools
|
||||
vorbis-tools
|
||||
|
|
|
@ -17,6 +17,7 @@ libmad-ocaml
|
|||
libopus0
|
||||
libportaudio2
|
||||
libpulse0
|
||||
libpq-dev
|
||||
libsamplerate0
|
||||
libsoundtouch-ocaml
|
||||
libtaglib-ocaml
|
||||
|
@ -49,6 +50,8 @@ python3-cairo
|
|||
rabbitmq-server
|
||||
systemd-sysv
|
||||
unzip
|
||||
uwsgi
|
||||
uwsgi-plugin-python3
|
||||
vorbisgain
|
||||
vorbis-tools
|
||||
vorbis-tools
|
||||
|
|
|
@ -23,6 +23,7 @@ libmad-ocaml
|
|||
libopus0
|
||||
libportaudio2
|
||||
libpulse0
|
||||
libpq-dev
|
||||
libsamplerate0
|
||||
libsoundtouch-ocaml
|
||||
libssl-dev
|
||||
|
@ -70,6 +71,8 @@ rabbitmq-server
|
|||
silan
|
||||
sysvinit-utils
|
||||
unzip
|
||||
uwsgi
|
||||
uwsgi-plugin-python3
|
||||
vorbisgain
|
||||
vorbis-tools
|
||||
xmlstarlet
|
||||
|
|
|
@ -23,6 +23,7 @@ libmad-ocaml
|
|||
libopus0
|
||||
libportaudio2
|
||||
libpulse0
|
||||
libpq-dev
|
||||
libsamplerate0
|
||||
libsoundtouch-ocaml
|
||||
libssl-dev
|
||||
|
@ -70,6 +71,8 @@ rabbitmq-server
|
|||
silan
|
||||
sysvinit-utils
|
||||
unzip
|
||||
uwsgi
|
||||
uwsgi-plugin-python3
|
||||
vorbisgain
|
||||
vorbis-tools
|
||||
vorbis-tools
|
||||
|
|
|
@ -17,6 +17,7 @@ libmad-ocaml
|
|||
libopus0
|
||||
libportaudio2
|
||||
libpulse0
|
||||
libpq-dev
|
||||
libsamplerate0
|
||||
libsoundtouch-ocaml
|
||||
libssl-dev
|
||||
|
@ -63,4 +64,6 @@ vorbisgain
|
|||
vorbis-tools
|
||||
vorbis-tools
|
||||
xmlstarlet
|
||||
uwsgi
|
||||
uwsgi-plugin-python3
|
||||
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 -*-
|
||||
|
||||
__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 time
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
import urllib.parse
|
||||
import requests
|
||||
import socket
|
||||
import logging
|
||||
import json
|
||||
import base64
|
||||
import traceback
|
||||
from configobj import ConfigObj
|
||||
|
||||
from .utils import RequestProvider, ApiRequest, get_protocol
|
||||
|
||||
AIRTIME_API_VERSION = "1.1"
|
||||
|
||||
|
||||
api_config = {}
|
||||
api_endpoints = {}
|
||||
|
||||
# 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
|
||||
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
|
||||
api_config['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_config['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_config['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_config['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_config['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_config['handle_watched_dir_missing'] = 'handle-watched-dir-missing/format/json/api_key/%%api_key%%/dir/%%dir%%'
|
||||
api_endpoints['media_setup_url'] = 'media-monitor-setup/format/json/api_key/{api_key}'
|
||||
api_endpoints['upload_recorded'] = 'upload-recorded/format/json/api_key/{api_key}/fileid/{fileid}/showinstanceid/{showinstanceid}'
|
||||
api_endpoints['update_media_url'] = 'reload-metadata/format/json/api_key/{api_key}/mode/{mode}'
|
||||
api_endpoints['list_all_db_files'] = 'list-all-files/format/json/api_key/{api_key}/dir_id/{dir_id}/all/{all}'
|
||||
api_endpoints['list_all_watched_dirs'] = 'list-all-watched-dirs/format/json/api_key/{api_key}'
|
||||
api_endpoints['add_watched_dir'] = 'add-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_endpoints['set_storage_dir'] = 'set-storage-dir/format/json/api_key/{api_key}/path/{path}'
|
||||
api_endpoints['update_fs_mount'] = 'update-file-system-mount/format/json/api_key/{api_key}'
|
||||
api_endpoints['reload_metadata_group'] = 'reload-metadata-group/format/json/api_key/{api_key}'
|
||||
api_endpoints['handle_watched_dir_missing'] = 'handle-watched-dir-missing/format/json/api_key/{api_key}/dir/{dir}'
|
||||
#show-recorder
|
||||
api_config['show_schedule_url'] = 'recorded-shows/format/json/api_key/%%api_key%%'
|
||||
api_config['upload_file_url'] = 'rest/media'
|
||||
api_config['upload_retries'] = '3'
|
||||
api_config['upload_wait'] = '60'
|
||||
api_endpoints['show_schedule_url'] = 'recorded-shows/format/json/api_key/{api_key}'
|
||||
api_endpoints['upload_file_url'] = 'rest/media'
|
||||
api_endpoints['upload_retries'] = '3'
|
||||
api_endpoints['upload_wait'] = '60'
|
||||
#pypo
|
||||
api_config['export_url'] = 'schedule/api_key/%%api_key%%'
|
||||
api_config['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_config['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_config['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_config['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_config['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_config['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_config['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_config['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_config['update_cue_values_by_silan'] = 'update-cue-values-by-silan/api_key/%%api_key%%'
|
||||
api_endpoints['export_url'] = 'schedule/api_key/{api_key}'
|
||||
api_endpoints['get_media_url'] = 'get-media/file/{file}/api_key/{api_key}'
|
||||
api_endpoints['update_item_url'] = 'notify-schedule-group-play/api_key/{api_key}/schedule_id/{schedule_id}'
|
||||
api_endpoints['update_start_playing_url'] = 'notify-media-item-start-play/api_key/{api_key}/media_id/{media_id}/'
|
||||
api_endpoints['get_stream_setting'] = 'get-stream-setting/format/json/api_key/{api_key}/'
|
||||
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_endpoints['update_source_status'] = 'update-source-status/format/json/api_key/{api_key}/sourcename/{sourcename}/status/{status}'
|
||||
api_endpoints['check_live_stream_auth'] = 'check-live-stream-auth/format/json/api_key/{api_key}/username/{username}/password/{password}/djtype/{djtype}'
|
||||
api_endpoints['get_bootstrap_info'] = 'get-bootstrap-info/format/json/api_key/{api_key}'
|
||||
api_endpoints['get_files_without_replay_gain'] = 'get-files-without-replay-gain/api_key/{api_key}/dir_id/{dir_id}'
|
||||
api_endpoints['update_replay_gain_value'] = 'update-replay-gain-value/format/json/api_key/{api_key}'
|
||||
api_endpoints['notify_webstream_data'] = 'notify-webstream-data/api_key/{api_key}/media_id/{media_id}/format/json'
|
||||
api_endpoints['notify_liquidsoap_started'] = 'rabbitmq-do-push/api_key/{api_key}/format/json'
|
||||
api_endpoints['get_stream_parameters'] = 'get-stream-parameters/api_key/{api_key}/format/json'
|
||||
api_endpoints['push_stream_stats'] = 'push-stream-stats/api_key/{api_key}/format/json'
|
||||
api_endpoints['update_stream_setting_table'] = 'update-stream-setting-table/api_key/{api_key}/format/json'
|
||||
api_endpoints['get_files_without_silan_value'] = 'get-files-without-silan-value/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['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):
|
||||
def __init__(self, logger=None,config_path='/etc/airtime/airtime.conf'):
|
||||
if logger is None: self.logger = logging
|
||||
|
@ -213,7 +82,7 @@ class AirtimeApiClient(object):
|
|||
try:
|
||||
self.config = ConfigObj(config_path)
|
||||
self.config.update(api_config)
|
||||
self.services = RequestProvider(self.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)
|
||||
|
@ -223,8 +92,11 @@ class AirtimeApiClient(object):
|
|||
except Exception: return -1
|
||||
|
||||
def __get_api_version(self):
|
||||
try: return self.services.version_url()['api_version']
|
||||
except Exception: return -1
|
||||
try:
|
||||
return self.services.version_url()['api_version']
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
return -1
|
||||
|
||||
def is_server_compatible(self, verbose=True):
|
||||
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)
|
||||
|
||||
setup(name='api_clients',
|
||||
version='1.0',
|
||||
description='Airtime API Client',
|
||||
url='http://github.com/sourcefabric/Airtime',
|
||||
author='sourcefabric',
|
||||
version='2.0.0',
|
||||
description='LibreTime API Client',
|
||||
url='http://github.com/LibreTime/Libretime',
|
||||
author='LibreTime Contributors',
|
||||
license='AGPLv3',
|
||||
packages=['api_clients'],
|
||||
scripts=[],
|
||||
install_requires=[
|
||||
'configobj'
|
||||
'configobj',
|
||||
'python-dateutil',
|
||||
],
|
||||
zip_safe=False,
|
||||
data_files=[])
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import unittest
|
||||
from api_clients.api_client import ApcUrl, UrlBadParam, IncompleteUrl
|
||||
from api_clients.utils import ApcUrl, UrlBadParam, IncompleteUrl
|
||||
|
||||
class TestApcUrl(unittest.TestCase):
|
||||
def test_init(self):
|
||||
|
@ -8,16 +8,16 @@ class TestApcUrl(unittest.TestCase):
|
|||
self.assertEqual(u.base_url, url)
|
||||
|
||||
def test_params_1(self):
|
||||
u = ApcUrl("/testing/%%key%%")
|
||||
u = ApcUrl("/testing/{key}")
|
||||
self.assertEqual(u.params(key='val').url(), '/testing/val')
|
||||
|
||||
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()
|
||||
self.assertEqual(full_url, '/testing/AAA/BBB/more_testing')
|
||||
|
||||
def test_params_ex(self):
|
||||
u = ApcUrl("/testing/%%key%%")
|
||||
u = ApcUrl("/testing/{key}")
|
||||
with self.assertRaises(UrlBadParam):
|
||||
u.params(bad_key='testing')
|
||||
|
||||
|
@ -26,5 +26,5 @@ class TestApcUrl(unittest.TestCase):
|
|||
self.assertEqual( ApcUrl(u).url(), u )
|
||||
|
||||
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()
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
import unittest
|
||||
import json
|
||||
from mock import MagicMock, patch
|
||||
from api_clients.api_client import ApcUrl, ApiRequest
|
||||
from api_clients.utils import ApcUrl, ApiRequest
|
||||
|
||||
class ResponseInfo:
|
||||
def get_content_type(self):
|
||||
return 'application/json'
|
||||
@property
|
||||
def headers(self):
|
||||
return {'content-type': 'application/json'}
|
||||
|
||||
def json(self):
|
||||
return {'ok', 'ok'}
|
||||
|
||||
class TestApiRequest(unittest.TestCase):
|
||||
def test_init(self):
|
||||
u = ApiRequest('request_name', ApcUrl('/test/ing'))
|
||||
self.assertEqual(u.name, "request_name")
|
||||
|
||||
def test_call(self):
|
||||
ret = json.dumps( {'ok':'ok'} )
|
||||
def test_call_json(self):
|
||||
ret = {'ok':'ok'}
|
||||
read = MagicMock()
|
||||
read.read = MagicMock(return_value=ret)
|
||||
read.info = MagicMock(return_value=ResponseInfo())
|
||||
read.headers = {'content-type': 'application/json'}
|
||||
read.json = MagicMock(return_value=ret)
|
||||
u = 'http://localhost/testing'
|
||||
with patch('urllib.request.urlopen') as mock_method:
|
||||
with patch('requests.get') as mock_method:
|
||||
mock_method.return_value = read
|
||||
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()
|
||||
|
|
|
@ -2,7 +2,8 @@ import unittest
|
|||
import json
|
||||
from mock import patch, MagicMock
|
||||
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):
|
||||
def setUp(self):
|
||||
|
@ -18,13 +19,17 @@ class TestRequestProvider(unittest.TestCase):
|
|||
self.assertTrue('general' in self.cfg)
|
||||
|
||||
def test_init(self):
|
||||
rp = RequestProvider(self.cfg)
|
||||
self.assertTrue( len( rp.available_requests() ) > 0 )
|
||||
rp = RequestProvider(self.cfg, {})
|
||||
self.assertEqual(len(rp.available_requests()), 0)
|
||||
|
||||
def test_contains(self):
|
||||
rp = RequestProvider(self.cfg)
|
||||
methods = ['upload_recorded', 'update_media_url', 'list_all_db_files']
|
||||
methods = {
|
||||
'upload_recorded': '/1/',
|
||||
'update_media_url': '/2/',
|
||||
'list_all_db_files': '/3/',
|
||||
}
|
||||
rp = RequestProvider(self.cfg, methods)
|
||||
for meth in methods:
|
||||
self.assertTrue( meth in rp.requests )
|
||||
self.assertTrue(meth in rp.requests)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import unittest
|
||||
import datetime
|
||||
import configparser
|
||||
from api_clients.api_client import get_protocol
|
||||
import unittest
|
||||
from api_clients import utils
|
||||
|
||||
def get_force_ssl(value, useConfigParser):
|
||||
config = {}
|
||||
|
@ -10,12 +11,23 @@ def get_force_ssl(value, useConfigParser):
|
|||
'base_port': 80,
|
||||
'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):
|
||||
def test_dict_config_empty_http(self):
|
||||
config = {'general': {}}
|
||||
protocol = get_protocol(config)
|
||||
protocol = utils.get_protocol(config)
|
||||
self.assertEqual(protocol, 'http')
|
||||
|
||||
def test_dict_config_http(self):
|
||||
|
@ -24,7 +36,7 @@ class TestGetProtocol(unittest.TestCase):
|
|||
'base_port': 80,
|
||||
},
|
||||
}
|
||||
protocol = get_protocol(config)
|
||||
protocol = utils.get_protocol(config)
|
||||
self.assertEqual(protocol, 'http')
|
||||
|
||||
def test_dict_config_https(self):
|
||||
|
@ -33,7 +45,7 @@ class TestGetProtocol(unittest.TestCase):
|
|||
'base_port': 443,
|
||||
},
|
||||
}
|
||||
protocol = get_protocol(config)
|
||||
protocol = utils.get_protocol(config)
|
||||
self.assertEqual(protocol, 'https')
|
||||
|
||||
def test_dict_config_force_https(self):
|
||||
|
@ -47,7 +59,7 @@ class TestGetProtocol(unittest.TestCase):
|
|||
def test_configparser_config_empty_http(self):
|
||||
config = configparser.ConfigParser()
|
||||
config['general'] = {}
|
||||
protocol = get_protocol(config)
|
||||
protocol = utils.get_protocol(config)
|
||||
self.assertEqual(protocol, 'http')
|
||||
|
||||
def test_configparser_config_http(self):
|
||||
|
@ -55,7 +67,7 @@ class TestGetProtocol(unittest.TestCase):
|
|||
config['general'] = {
|
||||
'base_port': 80,
|
||||
}
|
||||
protocol = get_protocol(config)
|
||||
protocol = utils.get_protocol(config)
|
||||
self.assertEqual(protocol, 'http')
|
||||
|
||||
def test_configparser_config_https(self):
|
||||
|
@ -63,7 +75,7 @@ class TestGetProtocol(unittest.TestCase):
|
|||
config['general'] = {
|
||||
'base_port': 443,
|
||||
}
|
||||
protocol = get_protocol(config)
|
||||
protocol = utils.get_protocol(config)
|
||||
self.assertEqual(protocol, 'https')
|
||||
|
||||
def test_configparser_config_force_https(self):
|
||||
|
@ -73,3 +85,5 @@ class TestGetProtocol(unittest.TestCase):
|
|||
self.assertEqual(get_force_ssl(value, True), 'https')
|
||||
for value in negative_values:
|
||||
self.assertEqual(get_force_ssl(value, True), 'http')
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
|
@ -28,7 +28,7 @@ from configobj import ConfigObj
|
|||
|
||||
# custom imports
|
||||
#from util import *
|
||||
from api_clients import *
|
||||
from api_clients import version1 as api_client
|
||||
|
||||
LOG_LEVEL = logging.INFO
|
||||
LOG_PATH = '/var/log/airtime/pypo/notify.log'
|
||||
|
|
|
@ -4,7 +4,7 @@ import os
|
|||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from api_clients.api_client import AirtimeApiClient
|
||||
from api_clients.version1 import AirtimeApiClient
|
||||
|
||||
def generate_liquidsoap_config(ss):
|
||||
data = ss['msg']
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
from api_clients import *
|
||||
from api_clients import version1 as api_client
|
||||
import sys
|
||||
|
||||
api_clients = api_client.AirtimeApiClient()
|
||||
|
|
|
@ -12,7 +12,7 @@ import sys
|
|||
import telnetlib
|
||||
import time
|
||||
|
||||
from api_clients import api_client
|
||||
from api_clients import version1 as api_client
|
||||
from configobj import ConfigObj
|
||||
from datetime import datetime
|
||||
from optparse import OptionParser
|
||||
|
|
|
@ -7,7 +7,7 @@ import traceback
|
|||
import logging
|
||||
import time
|
||||
|
||||
from api_clients import api_client
|
||||
from api_clients import version1 as api_client
|
||||
|
||||
class ListenerStat(Thread):
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ from queue import Empty
|
|||
from threading import Thread, Timer
|
||||
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
|
||||
|
||||
|
||||
|
@ -28,7 +29,7 @@ signal.signal(signal.SIGINT, keyboardInterruptHandler)
|
|||
|
||||
logging.captureWarnings(True)
|
||||
|
||||
POLL_INTERVAL = 480
|
||||
POLL_INTERVAL = 400
|
||||
|
||||
class PypoFetch(Thread):
|
||||
|
||||
|
@ -38,6 +39,7 @@ class PypoFetch(Thread):
|
|||
#Hacky...
|
||||
PypoFetch.ref = self
|
||||
|
||||
self.v1_api_client = v1_api_client.AirtimeApiClient()
|
||||
self.api_client = api_client.AirtimeApiClient()
|
||||
self.fetch_queue = pypoFetch_q
|
||||
self.push_queue = pypoPush_q
|
||||
|
@ -150,7 +152,7 @@ class PypoFetch(Thread):
|
|||
def set_bootstrap_variables(self):
|
||||
self.logger.debug('Getting information needed on bootstrap from Airtime')
|
||||
try:
|
||||
info = self.api_client.get_bootstrap_info()
|
||||
info = self.v1_api_client.get_bootstrap_info()
|
||||
except Exception as e:
|
||||
self.logger.exception('Unable to get bootstrap info.. Exiting pypo...')
|
||||
|
||||
|
@ -255,7 +257,7 @@ class PypoFetch(Thread):
|
|||
stream_id = info[0]
|
||||
status = info[1]
|
||||
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
|
||||
|
@ -343,7 +345,7 @@ class PypoFetch(Thread):
|
|||
media_item = media[key]
|
||||
if (media_item['type'] == 'file'):
|
||||
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['file_ready'] = False
|
||||
media_filtered[key] = media_item
|
||||
|
@ -434,10 +436,14 @@ class PypoFetch(Thread):
|
|||
self.logger.exception("Problem removing file '%s'" % f)
|
||||
|
||||
def manual_schedule_fetch(self):
|
||||
success, self.schedule_data = self.api_client.get_schedule()
|
||||
if success:
|
||||
try:
|
||||
self.schedule_data = self.api_client.get_schedule()
|
||||
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):
|
||||
success = False
|
||||
|
@ -452,7 +458,7 @@ class PypoFetch(Thread):
|
|||
# push metadata to TuneIn. We have to do this because TuneIn turns
|
||||
# off metadata if it does not receive a request every 5 minutes.
|
||||
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()
|
||||
|
||||
def main(self):
|
||||
|
|
|
@ -16,6 +16,7 @@ import configparser
|
|||
import json
|
||||
import hashlib
|
||||
from requests.exceptions import ConnectionError, HTTPError, Timeout
|
||||
from api_clients import version2 as api_client
|
||||
|
||||
CONFIG_PATH = '/etc/airtime/airtime.conf'
|
||||
|
||||
|
@ -31,6 +32,7 @@ class PypoFile(Thread):
|
|||
self.media = None
|
||||
self.cache_dir = os.path.join(config["cache_dir"], "scheduler")
|
||||
self._config = self.read_config_file(CONFIG_PATH)
|
||||
self.api_client = api_client.AirtimeApiClient()
|
||||
|
||||
def copy_file(self, media_item):
|
||||
"""
|
||||
|
@ -44,6 +46,8 @@ class PypoFile(Thread):
|
|||
dst_exists = True
|
||||
try:
|
||||
dst_size = os.path.getsize(dst)
|
||||
if dst_size == 0:
|
||||
dst_exists = False
|
||||
except Exception as e:
|
||||
dst_exists = False
|
||||
|
||||
|
@ -63,41 +67,16 @@ class PypoFile(Thread):
|
|||
|
||||
if do_copy:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
self.logger.error(response)
|
||||
raise Exception("%s - Error occurred downloading file" % response.status_code)
|
||||
|
||||
for chunk in response.iter_content(1024):
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
handle.write(chunk)
|
||||
|
||||
#make file world readable and owner writable
|
||||
|
|
|
@ -20,7 +20,7 @@ from queue import Empty, Queue
|
|||
|
||||
from threading import Thread
|
||||
|
||||
from api_clients import api_client
|
||||
from api_clients import version1 as api_client
|
||||
from .timeout import ls_timeout
|
||||
|
||||
logging.captureWarnings(True)
|
||||
|
|
|
@ -21,14 +21,15 @@ from threading import Thread
|
|||
|
||||
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):
|
||||
"""
|
||||
api_client returns the correct instance of AirtimeApiClient. Although there is only one
|
||||
instance to choose from at the moment.
|
||||
"""
|
||||
return apc.AirtimeApiClient(logger)
|
||||
return v1_api_client.AirtimeApiClient(logger)
|
||||
|
||||
# loading config file
|
||||
try:
|
||||
|
|
Loading…
Reference in New Issue