fix errors + add missing dep

This commit is contained in:
bkfox 2023-03-28 14:06:27 +02:00
parent 112770eddf
commit 20d21ab77b
31 changed files with 509 additions and 367 deletions

View File

@ -12,17 +12,11 @@ repos:
args: args:
- --line-length=79 - --line-length=79
- --exclude="""\.git|\.__pycache__|venv|_build|buck-out|build|dist""" - --exclude="""\.git|\.__pycache__|venv|_build|buck-out|build|dist"""
- repo: https://github.com/PyCQA/autoflake.git
rev: v2.0.2
hooks:
- id: autoflake
args:
- --remove-all-unused-imports
- repo: https://github.com/PyCQA/flake8.git - repo: https://github.com/PyCQA/flake8.git
rev: 6.0.0 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
exclude: instance/sample_settings.py exclude: instance/settings/
- repo: https://github.com/PyCQA/docformatter.git - repo: https://github.com/PyCQA/docformatter.git
rev: v1.5.1 rev: v1.5.1
hooks: hooks:

180
aircox/conf.py Executable file
View File

@ -0,0 +1,180 @@
import os
import inspect
from django.conf import settings as d_settings
__all__ = ("Settings", "settings")
# code from django-fox
class BaseSettings:
"""Utility class used to load and save settings, can be used as model.
Some members are excluded from being configuration:
- Protected/private members;
- On django model, "objects" and "Meta";
- Class declaration and callables
Example:
```
class MySettings(Settings):
a = 13
b = 12
my_settings = MySettings().load('MY_SETTINGS_KEY')
print(my_settings.a, my_settings.get('b'))
```
This will load values from django project settings.
"""
def __init__(self, key, module=None):
self.load(key, module)
def load(self, key, module=None):
"""Load settings from module's item specified by its member name. When
no module is provided, uses ``django.conf.settings``.
:param str key: module member name.
:param module: configuration object.
:returns self
"""
if module is None:
module = d_settings
settings = getattr(module, key, None)
if settings:
self.update(settings)
return self
def update(self, settings):
"""Update self's values from provided settings. ``settings`` can be an
iterable of ``(key, value)``.
:param dict|Settings|iterable settings: value to update from.
"""
if isinstance(settings, (dict, Settings)):
settings = settings.items()
for key, value in settings:
if self.is_config_item(key, value):
setattr(self, key, value)
def get(self, key, default=None):
"""Return settings' value for provided key."""
return getattr(self, key, default)
def items(self):
"""Iterate over items members, as tupple of ``key, value``."""
for key in dir(self):
value = getattr(self, key)
if self.is_config_item(key, value):
yield key, value
def is_config_item(self, key, value):
"""Return True if key/value item is a configuration setting."""
if key.startswith("_") or callable(value) or inspect.isclass(value):
return False
return True
class Settings(BaseSettings):
# --- Global & misc
DEFAULT_USER_GROUPS = {
"radio hosts": (
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"change_diffusion",
"add_comment",
"change_comment",
"delete_comment",
"add_article",
"change_article",
"delete_article",
"change_sound",
"add_track",
"change_track",
"delete_track",
# taggit
"add_tag",
"change_tag",
"delete_tag",
# filer
"add_folder",
"change_folder",
"delete_folder",
"can_use_directory_listing",
"add_image",
"change_image",
"delete_image",
),
}
"""Groups to assign to users at their creation, along with the permissions
to add to each group."""
PROGRAMS_DIR = "programs"
"""Directory for the programs data."""
@property
def PROGRAMS_DIR_ABS(self):
return os.path.join(d_settings.MEDIA_ROOT, self.PROGRAMS_DIR)
# --- Programs & episodes
EPISODE_TITLE = "{program.title} - {date}"
"""Default title for episodes."""
EPISODE_TITLE_DATE_FORMAT = "%-d %B %Y"
"""Date format in episode title (python's strftime)"""
# --- Logs & archives
LOGS_ARCHIVES_DIR = "logs/archives"
"""Directory where to save logs' archives."""
@property
def LOGS_ARCHIVES_DIR_ABS(self):
return os.path.join(d_settings.PROJECT_ROOT, self.LOGS_ARCHIVES_DIR)
LOGS_ARCHIVES_AGE = 60
"""In days, minimal age of a log before it is archived."""
# --- Sounds
SOUND_ARCHIVES_SUBDIR = "archives"
"""Sub directory used for the complete episode sounds."""
SOUND_EXCERPTS_SUBDIR = "excerpts"
"""Sub directory used for the excerpts of the episode."""
SOUND_QUALITY = {
"attribute": "RMS lev dB",
"range": (-18.0, -8.0),
"sample_length": 120,
}
"""Quality attributes passed to sound_quality_check from sounds_monitor
(Soxi parameters)."""
SOUND_FILE_EXT = (".ogg", ".flac", ".wav", ".mp3", ".opus")
"""Extension of sound files."""
SOUND_KEEP_DELETED = False
"""Tag sounds as deleted instead of deleting them when file has been
removed from filesystem (sound monitoring)."""
# --- Streamer & Controllers
CONTROLLERS_WORKING_DIR = "/tmp/aircox"
"""Controllers working directory."""
# --- Playlist import from CSV
IMPORT_PLAYLIST_CSV_COLS = (
"artist",
"title",
"minutes",
"seconds",
"tags",
"info",
)
"""Columns for CSV file."""
IMPORT_PLAYLIST_CSV_DELIMITER = ";"
"""Column delimiter of csv text files."""
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
"""Text delimiter of csv text files."""
settings = Settings("AIRCOX")

View File

@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone as tz from django.utils import timezone as tz
import aircox.settings as settings from aircox.conf import settings
from aircox.models import Log from aircox.models import Log
from aircox.models.log import LogArchiver from aircox.models.log import LogArchiver
@ -29,9 +29,9 @@ class Command(BaseCommand):
"-a", "-a",
"--age", "--age",
type=int, type=int,
default=settings.AIRCOX_LOGS_ARCHIVES_AGE, default=settings.LOGS_ARCHIVES_AGE,
help="minimal age in days of logs to archive. Default is " help="minimal age in days of logs to archive. Default is "
"settings.AIRCOX_LOGS_ARCHIVES_AGE", "settings.LOGS_ARCHIVES_AGE",
) )
group.add_argument( group.add_argument(
"-k", "-k",

View File

@ -2,9 +2,9 @@
sound. sound.
Playlists are in CSV format, where columns are separated with a Playlists are in CSV format, where columns are separated with a
'{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is '{settings.IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
{settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE}. {settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS} The order of the elements is: {settings.IMPORT_PLAYLIST_CSV_COLS}
If 'minutes' or 'seconds' are given, position will be expressed as timed If 'minutes' or 'seconds' are given, position will be expressed as timed
position, instead of position in playlist. position, instead of position in playlist.
@ -16,7 +16,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from aircox import settings from aircox.conf import settings
from aircox.models import Sound, Track from aircox.models import Sound, Track
__doc__ = __doc__.format(settings=settings) __doc__ = __doc__.format(settings=settings)
@ -61,9 +61,9 @@ class PlaylistImport:
) )
and row.strip() and row.strip()
), ),
fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS, fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER, delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
quotechar=settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE, quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
) )
) )
@ -80,7 +80,7 @@ class PlaylistImport:
) )
return return
maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS maps = settings.IMPORT_PLAYLIST_CSV_COLS
tracks = [] tracks = []
logger.info("parse csv file " + self.path) logger.info("parse csv file " + self.path)

View File

@ -20,7 +20,7 @@ Where:
To check quality of files, call the command sound_quality_check using the To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi). Sox (and soxi).
""" """
import atexit import atexit
@ -33,7 +33,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from watchdog.observers import Observer from watchdog.observers import Observer
from aircox import settings from aircox.conf import settings
from aircox.management.sound_file import SoundFile from aircox.management.sound_file import SoundFile
from aircox.management.sound_monitor import MonitorHandler from aircox.management.sound_monitor import MonitorHandler
from aircox.models import Program, Sound from aircox.models import Program, Sound
@ -67,12 +67,12 @@ class Command(BaseCommand):
logger.info("#%d %s", program.id, program.title) logger.info("#%d %s", program.id, program.title)
self.scan_for_program( self.scan_for_program(
program, program,
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, settings.SOUND_ARCHIVES_SUBDIR,
type=Sound.TYPE_ARCHIVE, type=Sound.TYPE_ARCHIVE,
) )
self.scan_for_program( self.scan_for_program(
program, program,
settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, settings.SOUND_EXCERPTS_SUBDIR,
type=Sound.TYPE_EXCERPT, type=Sound.TYPE_EXCERPT,
) )
dirs.append(os.path.join(program.abspath)) dirs.append(os.path.join(program.abspath))
@ -91,7 +91,7 @@ class Command(BaseCommand):
# sounds in directory # sounds in directory
for path in os.listdir(subdir): for path in os.listdir(subdir):
path = os.path.join(subdir, path) path = os.path.join(subdir, path)
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT): if not path.endswith(settings.SOUND_FILE_EXT):
continue continue
sound_file = SoundFile(path) sound_file = SoundFile(path)
@ -115,12 +115,12 @@ class Command(BaseCommand):
"""Run in monitor mode.""" """Run in monitor mode."""
with futures.ThreadPoolExecutor() as pool: with futures.ThreadPoolExecutor() as pool:
archives_handler = MonitorHandler( archives_handler = MonitorHandler(
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, settings.SOUND_ARCHIVES_SUBDIR,
pool, pool,
type=Sound.TYPE_ARCHIVE, type=Sound.TYPE_ARCHIVE,
) )
excerpts_handler = MonitorHandler( excerpts_handler = MonitorHandler(
settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, settings.SOUND_EXCERPTS_SUBDIR,
pool, pool,
type=Sound.TYPE_EXCERPT, type=Sound.TYPE_EXCERPT,
) )
@ -128,12 +128,12 @@ class Command(BaseCommand):
observer = Observer() observer = Observer()
observer.schedule( observer.schedule(
archives_handler, archives_handler,
settings.AIRCOX_PROGRAMS_DIR_ABS, settings.PROGRAMS_DIR_ABS,
recursive=True, recursive=True,
) )
observer.schedule( observer.schedule(
excerpts_handler, excerpts_handler,
settings.AIRCOX_PROGRAMS_DIR_ABS, settings.PROGRAMS_DIR_ABS,
recursive=True, recursive=True,
) )
observer.start() observer.start()

View File

@ -17,7 +17,7 @@ Where:
Sound Quality Sound Quality
============= =============
To check quality of files, call the command sound_quality_check using the To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi). Sox (and soxi).
""" """
import logging import logging

View File

@ -20,7 +20,7 @@ Where:
To check quality of files, call the command sound_quality_check using the To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi). Sox (and soxi).
""" """
import logging import logging
@ -29,7 +29,7 @@ from datetime import datetime, timedelta
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
from aircox import settings from aircox.conf import settings
from aircox.models import Sound from aircox.models import Sound
from .sound_file import SoundFile from .sound_file import SoundFile
@ -121,7 +121,7 @@ class MonitorHandler(PatternMatchingEventHandler):
def __init__(self, subdir, pool, **sync_kw): def __init__(self, subdir, pool, **sync_kw):
""" """
:param str subdir: sub-directory in program dirs to monitor \ :param str subdir: sub-directory in program dirs to monitor \
(AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR); (SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
:param concurrent.futures.Executor pool: pool executing jobs on file :param concurrent.futures.Executor pool: pool executing jobs on file
change; change;
:param **sync_kw: kwargs passed to `SoundFile.sync`; :param **sync_kw: kwargs passed to `SoundFile.sync`;
@ -132,7 +132,7 @@ class MonitorHandler(PatternMatchingEventHandler):
patterns = [ patterns = [
"*/{}/*{}".format(self.subdir, ext) "*/{}/*{}".format(self.subdir, ext)
for ext in settings.AIRCOX_SOUND_FILE_EXT for ext in settings.SOUND_FILE_EXT
] ]
super().__init__(patterns=patterns, ignore_directories=True) super().__init__(patterns=patterns, ignore_directories=True)

View File

@ -1,4 +1,3 @@
from . import signals
from .article import Article from .article import Article
from .episode import Diffusion, DiffusionQuerySet, Episode from .episode import Diffusion, DiffusionQuerySet, Episode
from .log import Log, LogArchiver, LogQuerySet from .log import Log, LogArchiver, LogQuerySet

View File

@ -7,7 +7,8 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.files import get_thumbnailer
from aircox import settings, utils from aircox.conf import settings
from aircox import utils
from .page import Page from .page import Page
from .program import ( from .program import (
@ -70,18 +71,18 @@ class Episode(Page):
@classmethod @classmethod
def get_default_title(cls, page, date): def get_default_title(cls, page, date):
return settings.AIRCOX_EPISODE_TITLE.format( return settings.EPISODE_TITLE.format(
program=page, program=page,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT), date=date.strftime(settings.EPISODE_TITLE_DATE_FORMAT),
) )
@classmethod @classmethod
def get_init_kwargs_from(cls, page, date, title=None, **kwargs): def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
"""Get default Episode's title.""" """Get default Episode's title."""
title = ( title = (
settings.AIRCOX_EPISODE_TITLE.format( settings.EPISODE_TITLE.format(
program=page, program=page,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT), date=date.strftime(settings.EPISODE_TITLE_DATE_FORMAT),
) )
if title is None if title is None
else title else title

View File

@ -10,8 +10,9 @@ from django.utils import timezone as tz
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from aircox import settings from aircox.conf import settings
__all__ = ("Settings", "settings")
from .episode import Diffusion from .episode import Diffusion
from .sound import Sound, Track from .sound import Sound, Track
from .station import Station from .station import Station
@ -251,7 +252,7 @@ class LogArchiver:
@staticmethod @staticmethod
def get_path(station, date): def get_path(station, date):
return os.path.join( return os.path.join(
settings.AIRCOX_LOGS_ARCHIVES_DIR, settings.LOGS_ARCHIVES_DIR_ABS,
"{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk), "{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk),
) )
@ -264,7 +265,7 @@ class LogArchiver:
if not qs.exists(): if not qs.exists():
return 0 return 0
os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok=True) os.makedirs(settings.LOGS_ARCHIVES_DIR_ABS, exist_ok=True)
count = qs.count() count = qs.count()
logs = self.sort_logs(qs) logs = self.sort_logs(qs)

View File

@ -15,7 +15,8 @@ from django.utils import timezone as tz
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from aircox import settings, utils from aircox import utils
from aircox.conf import settings
from .page import Page, PageQuerySet from .page import Page, PageQuerySet
from .station import Station from .station import Station
@ -77,9 +78,7 @@ class Program(Page):
@property @property
def path(self): def path(self):
"""Return program's directory path.""" """Return program's directory path."""
return os.path.join( return os.path.join(settings.PROGRAMS_DIR, self.slug.replace("-", "_"))
settings.AIRCOX_PROGRAMS_DIR, self.slug.replace("-", "_")
)
@property @property
def abspath(self): def abspath(self):
@ -88,11 +87,11 @@ class Program(Page):
@property @property
def archives_path(self): def archives_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR) return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
@property @property
def excerpts_path(self): def excerpts_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR) return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
def __init__(self, *kargs, **kwargs): def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().__init__(*kargs, **kwargs)
@ -107,8 +106,8 @@ class Program(Page):
We assume the path has been given in a previous time by this We assume the path has been given in a previous time by this
model (Program.path getter). model (Program.path getter).
""" """
if path.startswith(settings.AIRCOX_PROGRAMS_DIR_ABS): if path.startswith(settings.PROGRAMS_DIR_ABS):
path = path.replace(settings.AIRCOX_PROGRAMS_DIR_ABS, "") path = path.replace(settings.PROGRAMS_DIR_ABS, "")
while path[0] == "/": while path[0] == "/":
path = path[1:] path = path[1:]
path = path[: path.index("/")] path = path[: path.index("/")]

View File

@ -4,7 +4,8 @@ from django.db.models import signals
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone as tz from django.utils import timezone as tz
from .. import settings, utils from aircox import utils
from aircox.conf import settings
from . import Diffusion, Episode, Page, Program, Schedule from . import Diffusion, Episode, Page, Program, Schedule
@ -20,7 +21,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
if not created or instance.is_superuser: if not created or instance.is_superuser:
return return
for group_name, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items(): for group_name, permissions in settings.DEFAULT_USER_GROUPS.items():
if instance.groups.filter(name=group_name).count(): if instance.groups.filter(name=group_name).count():
continue continue

View File

@ -8,7 +8,7 @@ from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from aircox import settings from aircox.conf import settings
from .episode import Episode from .episode import Episode
from .program import Program from .program import Program
@ -123,9 +123,9 @@ class Sound(models.Model):
def _upload_to(self, filename): def _upload_to(self, filename):
subdir = ( subdir = (
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR settings.SOUND_ARCHIVES_SUBDIR
if self.type == self.TYPE_ARCHIVE if self.type == self.TYPE_ARCHIVE
else settings.AIRCOX_SOUND_EXCERPTS_SUBDIR else settings.SOUND_EXCERPTS_SUBDIR
) )
return os.path.join(self.program.path, subdir, filename) return os.path.join(self.program.path, subdir, filename)

View File

@ -5,7 +5,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField from filer.fields.image import FilerImageField
from .. import settings from aircox.conf import settings
__all__ = ("Station", "StationQuerySet", "Port") __all__ = ("Station", "StationQuerySet", "Port")
@ -92,7 +92,7 @@ class Station(models.Model):
def save(self, make_sources=True, *args, **kwargs): def save(self, make_sources=True, *args, **kwargs):
if not self.path: if not self.path:
self.path = os.path.join( self.path = os.path.join(
settings.AIRCOX_CONTROLLERS_WORKING_DIR, settings.CONTROLLERS_WORKING_DIR,
self.slug.replace("-", "_"), self.slug.replace("-", "_"),
) )

168
aircox/settings.py Executable file → Normal file
View File

@ -1,125 +1,73 @@
import os import inspect
from django.conf import settings from django.conf import settings as django_settings
from django.db import models
def ensure(key, default): class Settings:
value = getattr(settings, key, default) """Utility class used to load and save settings, can be used as model.
globals()[key] = value
return value
Some members are excluded from being configuration:
- Protected/private members;
- On django model, "objects" and "Meta";
- Class declaration and callables
######################################################################## Example:
# Global & misc
########################################################################
# group to assign to users at their creation, along with the permissions
# to add to each group.
ensure(
"AIRCOX_DEFAULT_USER_GROUPS",
{
"radio hosts": (
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"change_diffusion",
"add_comment",
"change_comment",
"delete_comment",
"add_article",
"change_article",
"delete_article",
"change_sound",
"add_track",
"change_track",
"delete_track",
# taggit
"add_tag",
"change_tag",
"delete_tag",
# filer
"add_folder",
"change_folder",
"delete_folder",
"can_use_directory_listing",
"add_image",
"change_image",
"delete_image",
),
},
)
# Directory for the programs data ```
AIRCOX_PROGRAMS_DIR = ensure("AIRCOX_PROGRAMS_DIR", "programs") class MySettings(Settings):
ensure( a = 13
"AIRCOX_PROGRAMS_DIR_ABS", b = 12
os.path.join(settings.MEDIA_ROOT, AIRCOX_PROGRAMS_DIR),
)
my_settings = MySettings().load('MY_SETTINGS_KEY')
print(my_settings.a, my_settings.get('b'))
```
######################################################################## This will load values from django project settings.
# Programs & Episodes """
########################################################################
# default title for episodes
ensure("AIRCOX_EPISODE_TITLE", "{program.title} - {date}")
# date format in episode title (python's strftime)
ensure("AIRCOX_EPISODE_TITLE_DATE_FORMAT", "%-d %B %Y")
######################################################################## def load(self, key, module=None):
# Logs & Archives """Load settings from module's item specified by its member name. When
######################################################################## no module is provided, uses ``django.conf.settings``.
# Directory where to save logs' archives
ensure(
"AIRCOX_LOGS_ARCHIVES_DIR",
os.path.join(settings.PROJECT_ROOT, "logs/archives"),
)
# In days, minimal age of a log before it is archived
ensure("AIRCOX_LOGS_ARCHIVES_AGE", 60)
:param str key: module member name.
:param module: configuration object.
:returns self
"""
if module is None:
module = django_settings
settings = getattr(module, key, None)
if settings:
self.update(settings)
return self
######################################################################## def update(self, settings):
# Sounds """Update self's values from provided settings. ``settings`` can be an
######################################################################## iterable of ``(key, value)``.
# Sub directory used for the complete episode sounds
ensure("AIRCOX_SOUND_ARCHIVES_SUBDIR", "archives")
# Sub directory used for the excerpts of the episode
ensure("AIRCOX_SOUND_EXCERPTS_SUBDIR", "excerpts")
# Quality attributes passed to sound_quality_check from sounds_monitor :param dict|Settings|iterable settings: value to update from.
ensure( """
"AIRCOX_SOUND_QUALITY", if isinstance(settings, (dict, Settings)):
{ settings = settings.items()
"attribute": "RMS lev dB", for key, value in settings:
"range": (-18.0, -8.0), if hasattr(self, key) and self.is_config_item(key, value):
"sample_length": 120, setattr(self, key, value)
},
)
# Extension of sound files def get(self, key, default=None):
ensure("AIRCOX_SOUND_FILE_EXT", (".ogg", ".flac", ".wav", ".mp3", ".opus")) """Return settings' value for provided key."""
return getattr(self, key, default)
# Tag sounds as deleted instead of deleting them when file has been removed def items(self):
# from filesystem (sound monitoring) """Iterate over items members, as tupple of ``key, value``."""
ensure("AIRCOX_SOUND_KEEP_DELETED", False) for key in dir(self):
value = getattr(self, key)
if self.is_config_item(key, value):
yield key, value
def is_config_item(self, key, value):
######################################################################## """Return True if key/value item is a configuration setting."""
# Streamer & Controllers if key.startswith("_") or callable(value) or inspect.isclass(value):
######################################################################## return False
# Controllers working directory if isinstance(self, models.Model) and key == "object":
ensure("AIRCOX_CONTROLLERS_WORKING_DIR", "/tmp/aircox") return False
return True
########################################################################
# Playlist import from CSV
########################################################################
# Columns for CSV file
ensure(
"AIRCOX_IMPORT_PLAYLIST_CSV_COLS",
("artist", "title", "minutes", "seconds", "tags", "info"),
)
# Column delimiter of csv text files
ensure("AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER", ";")
# Text delimiter of csv text files
ensure("AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE", '"')

View File

@ -1,15 +0,0 @@
from .sound_file import SoundFileTestCase
from .sound_monitor import (
ModifiedHandlerTestCase,
MonitorHandlerTestCase,
MoveHandlerTestCase,
NotifyHandlerTestCase,
)
__all__ = (
"SoundFileTestCase",
"NotifyHandlerTestCase",
"MoveHandlerTestCase",
"ModifiedHandlerTestCase",
"MonitorHandlerTestCase",
)

View File

@ -1,103 +0,0 @@
from datetime import timedelta
from django.conf import settings as conf
from django.test import TestCase
from django.utils import timezone as tz
from aircox import models
from aircox.management.sound_file import SoundFile
__all__ = ("SoundFileTestCase",)
class SoundFileTestCase(TestCase):
path_infos = {
"test/20220101_10h13_1_sample_1.mp3": {
"year": 2022,
"month": 1,
"day": 1,
"hour": 10,
"minute": 13,
"n": 1,
"name": "Sample 1",
},
"test/20220102_10h13_sample_2.mp3": {
"year": 2022,
"month": 1,
"day": 2,
"hour": 10,
"minute": 13,
"name": "Sample 2",
},
"test/20220103_1_sample_3.mp3": {
"year": 2022,
"month": 1,
"day": 3,
"n": 1,
"name": "Sample 3",
},
"test/20220104_sample_4.mp3": {
"year": 2022,
"month": 1,
"day": 4,
"name": "Sample 4",
},
"test/20220105.mp3": {
"year": 2022,
"month": 1,
"day": 5,
"name": "20220105",
},
}
subdir_prefix = "test"
sound_files = {
k: r
for k, r in (
(path, SoundFile(conf.MEDIA_ROOT + "/" + path))
for path in path_infos.keys()
)
}
def test_sound_path(self):
for path, sound_file in self.sound_files.items():
self.assertEqual(path, sound_file.sound_path)
def test_read_path(self):
for path, sound_file in self.sound_files.items():
expected = self.path_infos[path]
result = sound_file.read_path(path)
# remove None values
result = {k: v for k, v in result.items() if v is not None}
self.assertEqual(expected, result, "path: {}".format(path))
def _setup_diff(self, program, info):
episode = models.Episode(program=program, title="test-episode")
at = tz.datetime(
**{
k: info[k]
for k in ("year", "month", "day", "hour", "minute")
if info.get(k)
}
)
at = tz.make_aware(at)
diff = models.Diffusion(
episode=episode, start=at, end=at + timedelta(hours=1)
)
episode.save()
diff.save()
return diff
def test_find_episode(self):
station = models.Station(name="test-station")
program = models.Program(station=station, title="test")
station.save()
program.save()
for path, sound_file in self.sound_files.items():
infos = sound_file.read_path(path)
diff = self._setup_diff(program, infos)
sound = models.Sound(program=diff.program, file=path)
result = sound_file.find_episode(sound, infos)
self.assertEquals(diff.episode, result)
# TODO: find_playlist, sync

View File

@ -0,0 +1,110 @@
import pytest
from datetime import timedelta
from django.conf import settings as conf
from django.utils import timezone as tz
from aircox import models
from aircox.management.sound_file import SoundFile
@pytest.fixture
def path_infos():
return {
"test/20220101_10h13_1_sample_1.mp3": {
"year": 2022,
"month": 1,
"day": 1,
"hour": 10,
"minute": 13,
"n": 1,
"name": "Sample 1",
},
"test/20220102_10h13_sample_2.mp3": {
"year": 2022,
"month": 1,
"day": 2,
"hour": 10,
"minute": 13,
"name": "Sample 2",
},
"test/20220103_1_sample_3.mp3": {
"year": 2022,
"month": 1,
"day": 3,
"n": 1,
"name": "Sample 3",
},
"test/20220104_sample_4.mp3": {
"year": 2022,
"month": 1,
"day": 4,
"name": "Sample 4",
},
"test/20220105.mp3": {
"year": 2022,
"month": 1,
"day": 5,
"name": "20220105",
},
}
@pytest.fixture
def sound_files(path_infos):
return {
k: r
for k, r in (
(path, SoundFile(conf.MEDIA_ROOT + "/" + path))
for path in path_infos.keys()
)
}
def test_sound_path(sound_files):
for path, sound_file in sound_files.items():
assert path == sound_file.sound_path
def test_read_path(path_infos, sound_files):
for path, sound_file in sound_files.items():
expected = path_infos[path]
result = sound_file.read_path(path)
# remove None values
result = {k: v for k, v in result.items() if v is not None}
assert expected == result, "path: {}".format(path)
def _setup_diff(program, info):
episode = models.Episode(program=program, title="test-episode")
at = tz.datetime(
**{
k: info[k]
for k in ("year", "month", "day", "hour", "minute")
if info.get(k)
}
)
at = tz.make_aware(at)
diff = models.Diffusion(
episode=episode, start=at, end=at + timedelta(hours=1)
)
episode.save()
diff.save()
return diff
def test_find_episode(sound_files):
station = models.Station(name="test-station")
program = models.Program(station=station, title="test")
station.save()
program.save()
for path, sound_file in sound_files.items():
infos = sound_file.read_path(path)
diff = _setup_diff(program, infos)
sound = models.Sound(program=diff.program, file=path)
result = sound_file.find_episode(sound, infos)
assert diff.episode == result
# TODO: find_playlist, sync

View File

@ -1,22 +1,15 @@
import pytest
import concurrent.futures as futures import concurrent.futures as futures
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.test import TestCase
from aircox.management.sound_monitor import ( from aircox.management.sound_monitor import (
ModifiedHandler, ModifiedHandler,
MonitorHandler, MonitorHandler,
NotifyHandler, NotifyHandler,
) )
__all__ = (
"NotifyHandlerTestCase",
"MoveHandlerTestCase",
"ModifiedHandlerTestCase",
"MonitorHandlerTestCase",
)
class FakeEvent: class FakeEvent:
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -31,22 +24,28 @@ class WaitHandler(NotifyHandler):
pass pass
class NotifyHandlerTestCase(TestCase): @pytest.fixture
def monitor():
pool = futures.ThreadPoolExecutor(2)
return MonitorHandler("archives", pool)
class TestNotifyHandler:
pass pass
class MoveHandlerTestCase(TestCase): class TestMoveHandler:
pass pass
class ModifiedHandlerTestCase(TestCase): class TestModifiedHandler:
def test_wait(self): def test_wait(self):
handler = ModifiedHandler() handler = ModifiedHandler()
handler.timeout_delta = timedelta(seconds=0.1) handler.timeout_delta = timedelta(seconds=0.1)
start = datetime.now() start = datetime.now()
handler.wait() handler.wait()
delta = datetime.now() - start delta = datetime.now() - start
self.assertTrue(delta < handler.timeout_delta + timedelta(seconds=0.1)) assert delta < handler.timeout_delta + timedelta(seconds=0.1)
def test_wait_ping(self): def test_wait_ping(self):
pool = futures.ThreadPoolExecutor(1) pool = futures.ThreadPoolExecutor(1)
@ -57,28 +56,24 @@ class ModifiedHandlerTestCase(TestCase):
time.sleep(0.3) time.sleep(0.3)
handler.ping() handler.ping()
time.sleep(0.3) time.sleep(0.3)
self.assertTrue(future.running()) assert future.running()
class MonitorHandlerTestCase(TestCase): class TestMonitorHandler:
def setUp(self): def test_submit_new_job(self, monitor):
pool = futures.ThreadPoolExecutor(2)
self.monitor = MonitorHandler("archives", pool)
def test_submit_new_job(self):
event = FakeEvent(src_path="dummy_src") event = FakeEvent(src_path="dummy_src")
handler = NotifyHandler() handler = NotifyHandler()
result, _ = self.monitor._submit(handler, event, "up") result, _ = monitor._submit(handler, event, "up")
self.assertIs(handler, result) assert handler == result
self.assertIsInstance(handler.future, futures.Future) assert isinstance(handler.future, futures.Future)
self.monitor.pool.shutdown() monitor.pool.shutdown()
def test_submit_job_exists(self): def test_submit_job_exists(self, monitor):
event = FakeEvent(src_path="dummy_src") event = FakeEvent(src_path="dummy_src")
job_1, new_1 = self.monitor._submit(WaitHandler(), event, "up") job_1, new_1 = monitor._submit(WaitHandler(), event, "up")
job_2, new_2 = self.monitor._submit(NotifyHandler(), event, "up") job_2, new_2 = monitor._submit(NotifyHandler(), event, "up")
self.assertIs(job_1, job_2) assert job_1 == job_2
self.assertTrue(new_1) assert new_1
self.assertFalse(new_2) assert not new_2
self.monitor.pool.shutdown() monitor.pool.shutdown()

View File

@ -1,8 +1,7 @@
import datetime import datetime
import django.utils.timezone as tz import django.utils.timezone as tz
__all__ = [ __all__ = (
"Redirect", "Redirect",
"redirect", "redirect",
"date_range", "date_range",
@ -10,9 +9,10 @@ __all__ = [
"date_or_default", "date_or_default",
"to_timedelta", "to_timedelta",
"seconds_to_time", "seconds_to_time",
] )
# FIXME: usage & why we don't use Django's
class Redirect(Exception): class Redirect(Exception):
"""Redirect exception -- see `redirect()`.""" """Redirect exception -- see `redirect()`."""

View File

@ -11,7 +11,7 @@ from django.template.loader import render_to_string
from django.utils import timezone as tz from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from aircox import settings from aircox.conf import settings
from aircox.utils import to_seconds from aircox.utils import to_seconds
from .connector import Connector from .connector import Connector

View File

View File

@ -1,15 +1,3 @@
"""Django and Aircox instance settings. This file should be saved as
`settings.py` in the same directory as this one.
User MUST define the following values: `SECRET_KEY`, `ALLOWED_HOSTS`, `DATABASES`
The following environment variables are used in settings:
* `AIRCOX_DEBUG` (`DEBUG`): enable/disable debugging
For Django settings see:
https://docs.djangoproject.com/en/3.1/topics/settings/
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os import os
import sys import sys
@ -19,7 +7,8 @@ from django.utils import timezone
sys.path.insert(1, os.path.dirname(os.path.realpath(__file__))) sys.path.insert(1, os.path.dirname(os.path.realpath(__file__)))
# Project root directory # Project root directory
PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(__file__ + "/../../../")
# DEBUG mode # DEBUG mode
DEBUG = ( DEBUG = (
(os.environ["AIRCOX_DEBUG"].lower() in ("true", 1)) (os.environ["AIRCOX_DEBUG"].lower() in ("true", 1))
@ -38,12 +27,6 @@ LC_LOCALE = "en_US.UTF-8"
TIME_ZONE = os.environ.get("TZ") or "UTC" TIME_ZONE = os.environ.get("TZ") or "UTC"
########################################################################
#
# You MUST configure those values
#
########################################################################
# Secret key: you MUST put a consistent secret key. You can generate one # Secret key: you MUST put a consistent secret key. You can generate one
# at https://djecrety.ir/ # at https://djecrety.ir/
SECRET_KEY = "" SECRET_KEY = ""
@ -56,15 +39,11 @@ DATABASES = {
"TIMEZONE": TIME_ZONE, "TIMEZONE": TIME_ZONE,
} }
} }
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Allowed host for HTTP requests # Allowed host for HTTP requests
ALLOWED_HOSTS = ("127.0.0.1",) ALLOWED_HOSTS = ("127.0.0.1",)
########################################################################
#
# You CAN configure starting from here
#
########################################################################
# Assets and medias: # Assets and medias:
# In production, user MUST configure webserver in order to serve static # In production, user MUST configure webserver in order to serve static
@ -81,26 +60,6 @@ STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
# Path to media directory (by default in static's directory) # Path to media directory (by default in static's directory)
MEDIA_ROOT = os.path.join(STATIC_ROOT, "media") MEDIA_ROOT = os.path.join(STATIC_ROOT, "media")
# Include specific configuration depending of DEBUG
if DEBUG:
from .dev import *
else:
from .prod import *
# Enable caching using memcache
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "127.0.0.1:11211",
}
}
########################################################################
#
# You don't really need to configure what is happening below
#
########################################################################
# Enables internationalization and timezone # Enables internationalization and timezone
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
@ -112,7 +71,7 @@ try:
import locale import locale
locale.setlocale(locale.LC_ALL, LC_LOCALE) locale.setlocale(locale.LC_ALL, LC_LOCALE)
except: except Exception:
print( print(
"Can not set locale {LC}. Is it available on you system? Hint: " "Can not set locale {LC}. Is it available on you system? Hint: "
"Check /etc/locale.gen and rerun locale-gen as sudo if needed.".format( "Check /etc/locale.gen and rerun locale-gen as sudo if needed.".format(

View File

@ -1,5 +1,13 @@
import os import os
from .base import *
try:
from .settings import *
except ImportError:
pass
LOCALE_PATHS = ["aircox/locale", "aircox_streamer/locale"] LOCALE_PATHS = ["aircox/locale", "aircox_streamer/locale"]
LOGGING = { LOGGING = {

View File

@ -1,5 +1,10 @@
import os import os
try:
from .settings import *
except ImportError:
pass
LOGGING = { LOGGING = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
@ -30,3 +35,12 @@ LOGGING = {
}, },
}, },
} }
# Enable caching using memcache
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "127.0.0.1:11211",
}
}

View File

@ -0,0 +1,43 @@
"""Django and Aircox instance settings. This file should be saved as
`settings.py` in the same directory as this one.
User MUST define the following values: `SECRET_KEY`, `ALLOWED_HOSTS`, `DATABASES`
The following environment variables are used in settings:
* `AIRCOX_DEBUG` (`DEBUG`): enable/disable debugging
For Django settings see:
https://docs.djangoproject.com/en/3.1/topics/settings/
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
# Debug mode: set to True for dev
# DEBUG = False
LANGUAGE_CODE = "fr-BE"
LC_LOCALE = "fr_BE.UTF-8"
# Secret key: you MUST put a consistent secret key. You can generate one
# at https://djecrety.ir/
SECRET_KEY = ""
# Database configuration: defaults to db.sqlite3
# DATABASES
# Allowed host for HTTP requests
# ALLOWED_HOSTS = ('127.0.0.1',)
# When LC_LOCALE is set here, this code activates it.
# Otherwise it can be removed
try:
import locale
locale.setlocale(locale.LC_ALL, LC_LOCALE)
except Exception:
print(
"Can not set locale {LC}. Is it available on you system? Hint: "
"Check /etc/locale.gen and rerun locale-gen as sudo if needed.".format(
LC=LANGUAGE_CODE
)
)
pass

4
pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE = instance.settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py

View File

@ -3,6 +3,7 @@ djangorestframework~=3.13
django-model-utils>=4.2 django-model-utils>=4.2
django-filter~=22.1 django-filter~=22.1
django-content-editor~=6.3
django-filer~=2.2 django-filer~=2.2
django-honeypot~=1.0 django-honeypot~=1.0
django-taggit~=3.0 django-taggit~=3.0

2
requirements_tests.txt Normal file
View File

@ -0,0 +1,2 @@
pytest~=7.2
pytest-django~=4.5

View File

@ -1,4 +1,5 @@
#! /bin/sh #! /bin/sh
export DJANGO_SETTINGS_MODULE=instance.settings.prod
# aircox daily tasks: # aircox daily tasks:
# - diffusions monitoring for the current month # - diffusions monitoring for the current month

View File

@ -21,7 +21,7 @@ autostart = true
autorestart = true autorestart = true
stdout_logfile = /srv/www/aircox/logs/server.log stdout_logfile = /srv/www/aircox/logs/server.log
redirect_stderr = true redirect_stderr = true
environment=AIRCOX_DEBUG="False" environment=AIRCOX_DEBUG="False",DJANGO_SETTINGS_MODULE=instance.settings.prod
[program:aircox_sounds_monitor] [program:aircox_sounds_monitor]
command = /srv/www/aircox/scripts/launch_in_venv ./manage.py sounds_monitor -qsm command = /srv/www/aircox/scripts/launch_in_venv ./manage.py sounds_monitor -qsm
@ -31,7 +31,7 @@ autostart = true
autorestart = true autorestart = true
stdout_logfile = /srv/www/aircox/logs/sounds_monitor.log stdout_logfile = /srv/www/aircox/logs/sounds_monitor.log
redirect_stderr = true redirect_stderr = true
environment=AIRCOX_DEBUG="False" environment=AIRCOX_DEBUG="False",DJANGO_SETTINGS_MODULE=instance.settings.prod
[program:aircox_streamer] [program:aircox_streamer]
command = /srv/www/aircox/scripts/launch_in_venv ./manage.py streamer -crm command = /srv/www/aircox/scripts/launch_in_venv ./manage.py streamer -crm
@ -41,4 +41,4 @@ autostart = true
autorestart = true autorestart = true
stdout_logfile = /srv/www/aircox/logs/streamer.log stdout_logfile = /srv/www/aircox/logs/streamer.log
redirect_stderr = true redirect_stderr = true
environment=AIRCOX_DEBUG="False" environment=AIRCOX_DEBUG="False",DJANGO_SETTINGS_MODULE=instance.settings.prod