...
 
Commits (4)
......@@ -5,6 +5,7 @@ Définition des champs de modèle pour la gestion de la diffusion
import logging
import audiotools
from django.core import checks
from django.db.models.fields import files
from . import forms
......@@ -13,12 +14,20 @@ from . import sounds
LOG = logging.getLogger(__name__)
class SoundFieldFile(sounds.SoundFile, files.FieldFile):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
def delete(self, save=True, keep_file=False):
"""
Effectue la suppression du fichier dans le storage
keep_file: booléen, permet de controuner cette destruction
keep_file: booléen, permet de contourner cette destruction
"""
# Nettoie le cache des données associées au fichier ayant déjà été ouvert
if hasattr(self, '_duree_cache'):
del self._duree_cache
if hasattr(self, '_metadata_cache'):
del self._metadata_cache
if not keep_file:
super().delete(save)
......@@ -37,6 +46,35 @@ class SoundField(files.FileField):
def __init__(self, verbose_name=None, name=None, **kwargs):
super().__init__(verbose_name, name, **kwargs)
def check(self, **kwargs):
errors = super().check(**kwargs)
errors.extend(self._check_audiotools_library_installed())
return errors
def _check_audiotools_library_installed(self):
try:
import audiotools
except ImportError:
return [
checks.Error(
'Cannot use SoundField because audiotools is not installed.',
hint=('Get audiotools at https://pypi.python.org/pypi/audiotools '
'or run command "pip install fmoo-audiotools".'),
obj=self,
)
]
else:
return []
def get_internal_type(self):
return "SoundField"
def contribute_to_class(self, cls, name, **kwargs):
"""
Méthode assurant l'association entre la classe SoundField et son gestionnaire SoundFile
"""
super().contribute_to_class(cls, name, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': forms.SoundField}
defaults.update(kwargs)
......
......@@ -2,14 +2,19 @@
"""
Définition du modèle de données pour la gestion des musiques
"""
import os
import tempfile
from io import BytesIO
import logging
from django import forms
from django.core.exceptions import ValidationError
import logging
LOG = logging.getLogger(__name__)
import audiotools
from website import settings
class SoundField(forms.FileField):
""" Class SoundField disponible dans les formulaires """
def to_python(self, data):
......@@ -17,7 +22,6 @@ class SoundField(forms.FileField):
Vérifie que les données du champs de fichier d'upload contient bien
du son (n'importe quoi que la bilbiothèque 'audiotools' gère).
"""
import tempfile
file_handler = tempfile.NamedTemporaryFile()
f = super().to_python(data)
if f is None:
......@@ -36,9 +40,8 @@ class SoundField(forms.FileField):
str(data),
file))
try:
import audiotools
audiotools.open(file)
LOG.debug("Le fichier {!r} est bien un fichier son".format(str(data)))
LOG.debug("Le fichier {!r} est bien un fichier son".format(file))
except audiotools.UnsupportedFile as exc:
raise ValidationError(
self.error_messages['invalid_sound'],
......@@ -46,6 +49,61 @@ class SoundField(forms.FileField):
) from exc
finally:
file_handler.close()
# Conformation du fichier
self.conformation(file)
if hasattr(f, 'seek') and callable(f.seek):
f.seek(0)
return f
def conformation(self, file_path):
"""
Mise en conformité du fichier sonore
"""
LOG.debug("# CONFORMATION")
tmp_dir = os.path.join(settings.MEDIA_ROOT, settings.UPLOAD_DIR)
os.makedirs(name=tmp_dir, exist_ok=True)
fichier_destination = tempfile.NamedTemporaryFile(
dir=tmp_dir,
delete=False)
LOG.debug("## Fichier temporaire de travail {!r}".format(fichier_destination.name))
source_audio_file = audiotools.open(file_path)
if (settings.AUDIO_CONFIG['bits_per_sample'] == source_audio_file.bits_per_sample() and
settings.AUDIO_CONFIG['channels'] == source_audio_file.channels() and
settings.AUDIO_CONFIG['sample_rate'] == source_audio_file.sample_rate()
):
audio_convertisseur = source_audio_file.to_pcm()
else:
if source_audio_file.supports_to_pcm():
LOG.info("## Création du convertisseur au format '{} Hz, {} chan, {} bits'".format(
settings.AUDIO_CONFIG['sample_rate'],
settings.AUDIO_CONFIG['channels'],
settings.AUDIO_CONFIG['bits_per_sample']))
audio_convertisseur = audiotools.PCMConverter(
source_audio_file.to_pcm(),
sample_rate=settings.AUDIO_CONFIG['sample_rate'],
channels=settings.AUDIO_CONFIG['channels'],
channel_mask=settings.AUDIO_CONFIG['channel_mask'],
bits_per_sample=settings.AUDIO_CONFIG['bits_per_sample'])
else:
LOG.error("Le décodeur {decodeur!r} pour ce format {format!r} "
"n'est pas installé sur le système"
"".format(decodeur=source_audio_file.DESCRIPTION,
format=source_audio_file.NAME))
raise audiotools.DecodingError("Aucun décodeur disponible")
LOG.info("## Sélection du type de conteneur de sortie")
LOG.info(" Type: {!r}".format(settings.AUDIO_CONFIG['type']))
LOG.info(" Comprimer le fichier en {type!r} dans {file!r}".format(
file=fichier_destination.name,
type=settings.AUDIO_CONFIG['type']))
if settings.AUDIO_CONFIG['type'] == 'flac':
conteneur = getattr(audiotools, 'FlacAudio')
else: # settings.AUDIO_CONFIG['type'] == 'mp3'
conteneur = getattr(audiotools, 'MP3Audio')
conteneur.from_pcm(fichier_destination.name,
audio_convertisseur,
compression='insane')
os.rename(fichier_destination.name, file_path)
......@@ -3,9 +3,7 @@
Définition du modèle de données pour la gestion de la diffusion
"""
import os
import tempfile
import logging
import audiotools
import datetime
from django.db import models
from django.db.models import F
......@@ -15,7 +13,6 @@ from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist
from .fields import SoundField
from website.settings import (
MEDIA_ROOT,
UPLOAD_DIR,
MAX_REPETITIONS,
DELAI_AVANT_DIFFUSION,
......@@ -152,14 +149,9 @@ class ObjetSonore(models.Model):
"""
objet_sonore_id = models.AutoField(primary_key=True)
fichier = SoundField('fichier sonore',
upload_to='uploads',
help_text='Formats de fichiers autorisés : {}'.format(str(SoundField.available_formats).strip('][')))
CONFIG = {'sample_rate': 44100,
'bits_per_sample': 16,
'channels': 2,
'channel_mask': int(audiotools.ChannelMask.from_fields(front_left=True,
front_right=True)),
'type': 'mp3'}
upload_to=UPLOAD_DIR,
help_text='Formats de fichiers autorisés : {}'.format(
str(SoundField.available_formats).strip('][')))
def __str__(self):
try:
......@@ -174,58 +166,11 @@ class ObjetSonore(models.Model):
return False
fichier_disponible.boolean = True
def conformation(self):
"""
Mise en conformité du fichier sonore
"""
fichier_destination = tempfile.NamedTemporaryFile(dir=os.path.join(MEDIA_ROOT, UPLOAD_DIR))
source_audio_file = audiotools.open(self.fichier.path)
if (self.CONFIG['bits_per_sample'] == source_audio_file.bits_per_sample() and
self.CONFIG['channels'] == source_audio_file.channels() and
self.CONFIG['sample_rate'] == source_audio_file.sample_rate()
):
audio_convertisseur = source_audio_file.to_pcm()
else:
if source_audio_file.supports_to_pcm():
LOG.info("### Création du convertisseur au format")
LOG.info(" '{} Hz, {} chan, {} bits'".format(
self.CONFIG['sample_rate'],
self.CONFIG['channels'],
self.CONFIG['bits_per_sample']))
audio_convertisseur = audiotools.PCMConverter(
source_audio_file.to_pcm(),
sample_rate=self.CONFIG['sample_rate'],
channels=self.CONFIG['channels'],
channel_mask=self.CONFIG['channel_mask'],
bits_per_sample=self.CONFIG['bits_per_sample'])
else:
LOG.error("Le décodeur {decodeur!r} pour ce format {format!r} "
"n'est pas installé sur le système"
"".format(decodeur=source_audio_file.DESCRIPTION,
format=source_audio_file.NAME))
raise audiotools.DecodingError("Aucun décodeur disponible")
LOG.info("### Sélection du type de conteneur de sortie")
LOG.info(" Type: {!r}".format(self.CONFIG['type']))
LOG.info(" Comprimer le fichier en {type}: {file}".format(
file=fichier_destination,
type=self.CONFIG['type']))
if self.CONFIG['type'] == 'flac':
conteneur = getattr(audiotools, 'FlacAudio')
else: # self.CONFIG['type'] == 'mp3'
conteneur = getattr(audiotools, 'MP3Audio')
conteneur.from_pcm(fichier_destination,
audio_convertisseur,
compression='insane')
def save(self, *args, **kwargs):
LOG.debug("START ObjetSonore.save()")
if self.objet_sonore_id is not None:
if self.fichier != ObjetSonore.objects.get(objet_sonore_id=self.objet_sonore_id).fichier:
self.conformation()
super().save(*args, **kwargs)
if self.fichier_disponible():
LOG.debug("- Nom dans la base {!r}".format(self.fichier))
LOG.debug("# Chemin du fichier de l'ObjetSonore {!r}".format(self.fichier.path))
LOG.debug("END ObjetSonore.save()")
def delete(self, save=True, keep_ObjetSonore=False, **kwargs):
......
This diff is collapsed.
......@@ -4,6 +4,8 @@ Utility functions for handling sounds.
Requires audiotools as you might imagine.
"""
import audiotools
import logging
LOG = logging.getLogger(__name__)
......@@ -12,60 +14,53 @@ from django.core.files import File
class SoundFile(File):
"""
A mixin for use alongside django.core.files.base.File, which provides
additional features for dealing with sounds.
Un mixin à utiliser aux côtés de django.core.files.base.File, fournissant
des fonctionnalités spécifiques à la manipulation de fichiers sonores.
"""
@property
def duree(self):
return self._get_sound_duree()
@property
def metadata(self):
return self._get_sound_metadata()
def _get_sound_duree(self):
if not hasattr(self, '_duree_cache'):
close = self.closed
self.open()
self._duree_cache = get_sound_duree(self.path, close=close)
self._duree_cache = get_sound_duree(self.path)
return self._duree_cache
@property
def metadata(self):
return self._get_sound_metadata()
def _get_sound_metadata(self):
if not hasattr(self, '_metadata_cache'):
close = self.closed
self.open()
self._metadata_cache = get_sound_metadata(self.path, close=close)
self._metadata_cache = get_sound_metadata(self.path)
return self._metadata_cache
def get_sound_duree(file_or_path, close=False):
def get_sound_duree(file_path):
"""
Return the duree of a sound, given an open file or a path. Set
'close' to True to close the file at the end if it is initially in an open
state.
Retourne la durée, exprimée en secondes, du fichier sonore
dont le chemin est fourni en paramètre.
"""
import audiotools
try:
LOG.debug("Recherche de la durée de {!r}".format(file_or_path))
f = audiotools.open(file_or_path)
LOG.debug("Recherche de la durée de {!r}".format(file_path))
f = audiotools.open(file_path)
frac_length = f.seconds_length()
return frac_length.numerator // frac_length.denominator
except:
return 0
def get_sound_metadata(file_or_path, close=False):
def get_sound_metadata(file_path):
"""
Return the metadata of a sound, given an open file or a path. Set
'close' to True to close the file at the end if it is initially in an open
state.
Retourne le 'audiotools.MetaData' du fichier sonore dont le
chemin est fourni en paramètre.
"""
import audiotools
try:
LOG.debug("Recherche des metadata de {!r}".format(file_or_path))
f = audiotools.open(file_or_path)
LOG.debug("Recherche des metadata de {!r}".format(file_path))
f = audiotools.open(file_path)
metadata = f.get_metadata()
return metadata
except:
return audiotools.MetaData()
\ No newline at end of file
return audiotools.MetaData()
......@@ -64,9 +64,10 @@ class Emission(models.Model):
sont disponibles sur le système de fichiers
"""
duree = datetime.timedelta(0)
for episode in self.episode_set_emission.all():
if episode.fichier_disponible() is True:
duree += episode.duree
if self.episode_set_emission.exists():
for episode in self.episode_set_emission.all():
if episode.fichier_disponible() is True:
duree += episode.duree
return duree
def __str__(self):
......
......@@ -11,6 +11,7 @@ https://docs.djangoproject.com/fr/2.0/ref/settings/
import os
import datetime
import audiotools
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
......@@ -148,10 +149,30 @@ USE_TZ = True
STATIC_URL = '/static/'
FIXTURE_DIRS = (os.path.join(BASE_DIR, 'auth', 'fixtures'),)
# Racine du stockage des fichiers
MEDIA_ROOT = '/radio'
# Répertoire de stockage pour les fichiers téléchargés
UPLOAD_DIR = 'uploads'
# Répertoire intermédiaire définissant la base audio des Diffusables
AUDIO_DIR = 'base-audio-diffusion'
# Configuration des paramètres généraux de conformation
AUDIO_CONFIG = {
'sample_rate': 44100,
'bits_per_sample': 16,
'channels': 2,
'channel_mask': int(audiotools.ChannelMask.from_fields(front_left=True,
front_right=True)),
'type': 'mp3',
}
# Nom à utiliser dans le cadre de la production d'Episode
# Le cas générale étant qu'un Episode est produit par l'organisation,
# par conséquent le medium de provenance de l'Episode est toujours le même
# et son nom est défini ici
PROVENANCE_INTERNE_NOM = 'Production interne'
# Nombre maximal de répétitions autorisé pour une programmation
MAX_REPETITIONS = 15
......