forked from rc/aircox
add script to import playlists to sounds or to diffusions
This commit is contained in:
154
programs/management/commands/import_playlist.py
Normal file
154
programs/management/commands/import_playlist.py
Normal 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
|
||||
)
|
||||
)
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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', '"')
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
|
||||
|
||||
def to_timedelta (time):
|
||||
"""
|
||||
Transform a datetime or a time instance to a timedelta,
|
||||
|
Reference in New Issue
Block a user