add script to import playlists to sounds or to diffusions

This commit is contained in:
bkfox
2016-07-15 18:23:08 +02:00
parent f458583f68
commit 161af3fb1a
12 changed files with 393 additions and 122 deletions

View File

@ -0,0 +1,154 @@
"""
Import one or more playlist for the given sound. Attach it to the sound
or to the related Diffusion if wanted.
We support different formats:
- plain text: a track per line, where columns are separated with a
'{settings.AIRCOX_IMPORT_PLAYLIST_PLAIN_DELIMITER}'.
The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_PLAIN_COLS}
- csv: CSV file where columns are separated with a
'{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER)}'. Text quote is
{settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS}
If 'minutes' or 'seconds' are given, position will be expressed as timed
position, instead of position in playlist.
Base the format detection using file extension. If '.csv', uses CSV importer,
otherwise plain text one.
"""
import os
import csv
import logging
from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
from aircox.programs.models import *
import aircox.programs.settings as settings
__doc__ = __doc__.format(settings)
logger = logging.getLogger('aircox.tools')
class Importer:
type = None
data = None
tracks = None
def __init__(self, related = None, path = None):
if path:
self.read(path)
if related:
self.make_playlist(related, True)
def reset(self):
self.type = None
self.data = None
self.tracks = None
def read(self, path):
if not os.path.exists(path):
return True
with open(path, 'r') as file:
sp, *ext = os.path.splitext(path)[1]
if ext[0] and ext[0] == 'csv':
self.type = 'csv'
self.data = csv.reader(
file,
delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
quotechar = settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
)
else:
self.type = 'plain'
self.data = [
line.split(settings.AIRCOX_IMPORT_PLAYLIST_PLAIN_DELIMITER)
for line in file.readlines()
]
def __get(self, line, field, default = None):
maps = settings.AIRCOX_IMPORT_CSV_COLS
if field not in maps:
return default
index = maps.index(field)
return line[index] if index < len(line) else default
def make_playlist(self, related, save = False):
"""
Make a playlist from the read data, and return it. If save is
true, save it into the database
"""
maps = settings.AIRCOX_IMPORT_CSV_COLS if self.type == 'csv' else \
settings.AIRCOX_IMPORT_PLAIN_COLS
tracks = []
for index, line in enumerate(self.data):
if ('minutes' or 'seconds') in maps:
kwargs['pos_in_secs'] = True
kwargs['pos'] = int(self.__get(line, 'minutes', 0)) * 60 + \
int(self.__get(line, 'seconds', 0))
else:
kwargs['pos'] = index
kwargs['related'] = related
kwargs.update({
k: self.__get(line, k) for k in maps
if k not in ('minutes', 'seconds')
})
track = Track(**kwargs)
# FIXME: bulk_create?
if save:
track.save()
tracks.append(track)
self.tracks = tracks
return tracks
class Command (BaseCommand):
help= __doc__
def add_arguments (self, parser):
parser.formatter_class=RawTextHelpFormatter
now = tz.datetime.today()
parser.add_argument(
'path', type=str,
help='path of the input playlist to read'
)
parser.add_argument(
'--sound', '-s', type=str,
help='generate a playlist for the sound of the given path. '
'If not given, try to match a sound with the same path.'
)
parser.add_argument(
'--diffusion', '-d', action='store_true',
help='try to get the diffusion relative to the sound if it exists'
)
def handle (self, path, *args, **options):
# FIXME: absolute/relative path of sounds vs given path
if options.get('sound'):
related = Sound.objects.filter(path__icontains = path).first()
else:
path, ext = os.path.splitext(options.get('path'))
related = Sound.objects.filter(path__icontains = path).first()
if not related:
logger.error('no sound found in the database for the path ' \
'{path}'.format(path=path))
return -1
if options.get('diffusion') and related.diffusion:
related = related.diffusion
importer = Importer(related = related, path = path)
for track in importer.tracks:
logger.log('imported track at {pos}: {name}, by '
'{artist}'.format(
pos = track.pos,
name = track.name, artist = track.artist
)
)

View File

@ -44,6 +44,48 @@ def date_or_default(date, no_time = False):
return date
class Related(models.Model):
"""
Add a field "related" of type GenericForeignKey, plus utilities.
"""
related_type = models.ForeignKey(
ContentType,
blank = True, null = True,
)
related_id = models.PositiveIntegerField(
blank = True, null = True,
)
related = GenericForeignKey(
'related_type', 'related_id',
)
@classmethod
def get_for(cl, object = None, model = None):
"""
Return a queryset that filter on the given object or model(s)
* object: if given, use its type and pk; match on models only.
* model: one model or list of models
"""
if not model and object:
model = type(object)
if type(model) in (list, tuple):
model = [ ContentType.objects.get_for_model(m).id
for m in model ]
qs = cl.objects.filter(related_type__pk__in = model)
else:
model = ContentType.objects.get_for_model(model)
qs = cl.objects.filter(related_type__pk = model.id)
if object:
qs = qs.filter(related_id = object.pk)
return qs
class Meta:
abstract = True
class Nameable(models.Model):
name = models.CharField (
_('name'),
@ -66,40 +108,6 @@ class Nameable(models.Model):
abstract = True
class Track(Nameable):
"""
Track of a playlist of a diffusion. The position can either be expressed
as the position in the playlist or as the moment in seconds it started.
"""
# There are no nice solution for M2M relations ship (even without
# through) in django-admin. So we unfortunately need to make one-
# to-one relations and add a position argument
diffusion = models.ForeignKey(
'Diffusion',
)
artist = models.CharField(
_('artist'),
max_length = 128,
)
# position can be used to specify a position in seconds for stream
# programs or a position in the playlist
position = models.SmallIntegerField(
default = 0,
help_text=_('position in the playlist'),
)
tags = TaggableManager(
verbose_name=_('tags'),
blank=True,
)
def __str__(self):
return ' '.join([self.artist, ':', self.name ])
class Meta:
verbose_name = _('Track')
verbose_name_plural = _('Tracks')
class Sound(Nameable):
"""
A Sound is the representation of a sound file that can be either an excerpt
@ -114,6 +122,7 @@ class Sound(Nameable):
'Diffusion',
verbose_name = _('diffusion'),
blank = True, null = True,
help_text = _('this is set for scheduled programs')
)
type = models.SmallIntegerField(
verbose_name = _('type'),
@ -713,3 +722,45 @@ class Diffusion(models.Model):
('programming', _('edit the diffusion\'s planification')),
)
class Track(Nameable,Related):
"""
Track of a playlist of an object. The position can either be expressed
as the position in the playlist or as the moment in seconds it started.
"""
# There are no nice solution for M2M relations ship (even without
# through) in django-admin. So we unfortunately need to make one-
# to-one relations and add a position argument
artist = models.CharField(
_('artist'),
max_length = 128,
)
position = models.SmallIntegerField(
default = 0,
help_text=_('position in the playlist'),
)
info = models.CharField(
_('information'),
max_length = 128,
blank = True, null = True,
help_text=_('additional informations about this track, such as '
'the version, if is it a remix, features, etc.'),
)
tags = TaggableManager(
verbose_name=_('tags'),
blank=True,
)
pos_in_secs = models.BooleanField(
_('use seconds'),
default = False,
help_text=_('position in the playlist is expressed in seconds')
)
def __str__(self):
return ' '.join([self.artist, ':', self.name ])
class Meta:
verbose_name = _('Track')
verbose_name_plural = _('Tracks')

View File

@ -37,9 +37,33 @@ ensure('AIRCOX_SOUND_QUALITY', {
)
# Extension of sound files
ensure('AIRCOX_SOUND_FILE_EXT',
('.ogg','.flac','.wav','.mp3','.opus'))
ensure(
'AIRCOX_SOUND_FILE_EXT',
('.ogg','.flac','.wav','.mp3','.opus')
)
# Stream for the scheduled diffusions
ensure('AIRCOX_SCHEDULED_STREAM', 0)
# Import playlist: columns for plain text files
ensure(
'AIRCOX_IMPORT_PLAYLIST_PLAIN_COLS',
('artist', 'title', 'tags', 'version')
)
# Import playlist: delimiter for plain text files
ensure('AIRCOX_IMPORT_PLAYLIST_PLAIN_DELIMITER', '--')
# Import playlist: columns for CSV file
ensure(
'AIRCOX_IMPORT_PLAYLIST_CSV_COLS',
('artist', 'title', 'minutes', 'seconds', 'tags', 'version')
)
# Import playlist: column delimiter of csv text files
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';')
# Import playlist: text delimiter of csv text files
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"')

View File

@ -1,5 +1,6 @@
import datetime
def to_timedelta (time):
"""
Transform a datetime or a time instance to a timedelta,