@ -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
									
								
							
							
						
						
									
										180
									
								
								aircox/conf.py
									
									
									
									
									
										Executable 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")
 | 
				
			||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -175,7 +175,7 @@ class SoundFile:
 | 
				
			|||||||
            at = tz.datetime(
 | 
					            at = tz.datetime(
 | 
				
			||||||
                year, month, day, pi.get("hour", 0), pi.get("minute", 0)
 | 
					                year, month, day, pi.get("hour", 0), pi.get("minute", 0)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            at = tz.get_current_timezone().localize(at)
 | 
					            at = tz.make_aware(at)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            at = date(year, month, day)
 | 
					            at = date(year, month, day)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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("/")]
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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
									
								
							
							
						
						
									
										168
									
								
								aircox/settings.py
									
									
									
									
									
										
										
										Executable file → Normal 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", '"')
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +0,0 @@
 | 
				
			|||||||
from .sound_file import SoundFileTestCase
 | 
					 | 
				
			||||||
from .sound_monitor import (
 | 
					 | 
				
			||||||
    ModifiedHandlerTestCase,
 | 
					 | 
				
			||||||
    MonitorHandlerTestCase,
 | 
					 | 
				
			||||||
    MoveHandlerTestCase,
 | 
					 | 
				
			||||||
    NotifyHandlerTestCase,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__all__ = (
 | 
					 | 
				
			||||||
    "SoundFileTestCase",
 | 
					 | 
				
			||||||
    "NotifyHandlerTestCase",
 | 
					 | 
				
			||||||
    "MoveHandlerTestCase",
 | 
					 | 
				
			||||||
    "ModifiedHandlerTestCase",
 | 
					 | 
				
			||||||
    "MonitorHandlerTestCase",
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					 | 
				
			||||||
							
								
								
									
										111
									
								
								aircox/tests/management/test_sound_file.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								aircox/tests/management/test_sound_file.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.django_db(transaction=True)
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
@ -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()
 | 
				
			||||||
@ -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()`."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								instance/settings/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								instance/settings/__init__.py
									
									
									
									
									
										Normal 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(
 | 
				
			||||||
@ -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 = {
 | 
				
			||||||
@ -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",
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								instance/settings/sample.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								instance/settings/sample.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										4
									
								
								pytest.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					[pytest]
 | 
				
			||||||
 | 
					DJANGO_SETTINGS_MODULE = instance.settings
 | 
				
			||||||
 | 
					# -- recommended but optional:
 | 
				
			||||||
 | 
					python_files = tests.py test_*.py *_tests.py
 | 
				
			||||||
@ -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
									
								
							
							
						
						
									
										2
									
								
								requirements_tests.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					pytest~=7.2
 | 
				
			||||||
 | 
					pytest-django~=4.5
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user