playlist import -- fixes and integration into the sound monitor

This commit is contained in:
bkfox 2016-07-16 23:51:53 +02:00
parent 161af3fb1a
commit 32a30004d6
7 changed files with 75 additions and 70 deletions

View File

@ -166,5 +166,9 @@ class ScheduleAdmin(admin.ModelAdmin):
list_display = ['id', 'program_name', 'frequency', 'date', 'day', 'rerun'] list_display = ['id', 'program_name', 'frequency', 'date', 'day', 'rerun']
list_editable = ['frequency', 'date'] list_editable = ['frequency', 'date']
admin.site.register(Track)
@admin.register(Track)
class TrackAdmin(admin.ModelAdmin):
list_display = ['id', 'title', 'artist', 'position', 'pos_in_secs', 'related']

View File

@ -2,20 +2,13 @@
Import one or more playlist for the given sound. Attach it to the sound Import one or more playlist for the given sound. Attach it to the sound
or to the related Diffusion if wanted. or to the related Diffusion if wanted.
We support different formats: Playlists are in CSV format, where columns are separated with a
- plain text: a track per line, where columns are separated with a '{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
'{settings.AIRCOX_IMPORT_PLAYLIST_PLAIN_DELIMITER}'. {settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_PLAIN_COLS} The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_CSV_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 If 'minutes' or 'seconds' are given, position will be expressed as timed
position, instead of position in playlist. 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 os
import csv import csv
@ -23,52 +16,41 @@ import logging
from argparse import RawTextHelpFormatter from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.contenttypes.models import ContentType
from aircox.programs.models import * from aircox.programs.models import *
import aircox.programs.settings as settings import aircox.programs.settings as settings
__doc__ = __doc__.format(settings) __doc__ = __doc__.format(settings = settings)
logger = logging.getLogger('aircox.tools') logger = logging.getLogger('aircox.tools')
class Importer: class Importer:
type = None
data = None data = None
tracks = None tracks = None
def __init__(self, related = None, path = None): def __init__(self, related = None, path = None, save = False):
if path: if path:
self.read(path) self.read(path)
if related: if related:
self.make_playlist(related, True) self.make_playlist(related, save)
def reset(self): def reset(self):
self.type = None
self.data = None self.data = None
self.tracks = None self.tracks = None
def read(self, path): def read(self, path):
if not os.path.exists(path): if not os.path.exists(path):
return True return True
with open(path, 'r') as file: with open(path, 'r') as file:
sp, *ext = os.path.splitext(path)[1] self.data = list(csv.reader(
if ext[0] and ext[0] == 'csv': file,
self.type = 'csv' delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
self.data = csv.reader( quotechar = settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
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): def __get(self, line, field, default = None):
maps = settings.AIRCOX_IMPORT_CSV_COLS maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
if field not in maps: if field not in maps:
return default return default
index = maps.index(field) index = maps.index(field)
@ -79,26 +61,30 @@ class Importer:
Make a playlist from the read data, and return it. If save is Make a playlist from the read data, and return it. If save is
true, save it into the database true, save it into the database
""" """
maps = settings.AIRCOX_IMPORT_CSV_COLS if self.type == 'csv' else \ maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
settings.AIRCOX_IMPORT_PLAIN_COLS
tracks = [] tracks = []
pos_in_secs = ('minutes' or 'seconds') in maps
for index, line in enumerate(self.data): for index, line in enumerate(self.data):
if ('minutes' or 'seconds') in maps: position = \
kwargs['pos_in_secs'] = True int(self.__get(line, 'minutes', 0)) * 60 + \
kwargs['pos'] = int(self.__get(line, 'minutes', 0)) * 60 + \ int(self.__get(line, 'seconds', 0)) \
int(self.__get(line, 'seconds', 0)) if pos_in_secs else index
else:
kwargs['pos'] = index
kwargs['related'] = related track, created = Track.objects.get_or_create(
kwargs.update({ related_type = ContentType.objects.get_for_model(related),
k: self.__get(line, k) for k in maps related_id = related.pk,
if k not in ('minutes', 'seconds') title = self.__get(line, 'title'),
}) artist = self.__get(line, 'artist'),
position = position,
)
track.pos_in_secs = pos_in_secs
track.info = self.__get(line, 'info')
tags = self.__get(line, 'tags')
if tags:
track.tags.add(*tags.split(','))
track = Track(**kwargs)
# FIXME: bulk_create?
if save: if save:
track.save() track.save()
tracks.append(track) tracks.append(track)
@ -114,7 +100,7 @@ class Command (BaseCommand):
now = tz.datetime.today() now = tz.datetime.today()
parser.add_argument( parser.add_argument(
'path', type=str, 'path', metavar='PATH', type=str,
help='path of the input playlist to read' help='path of the input playlist to read'
) )
parser.add_argument( parser.add_argument(
@ -130,7 +116,9 @@ class Command (BaseCommand):
def handle (self, path, *args, **options): def handle (self, path, *args, **options):
# FIXME: absolute/relative path of sounds vs given path # FIXME: absolute/relative path of sounds vs given path
if options.get('sound'): if options.get('sound'):
related = Sound.objects.filter(path__icontains = path).first() related = Sound.objects.filter(
path__icontains = options.get('sound')
).first()
else: else:
path, ext = os.path.splitext(options.get('path')) path, ext = os.path.splitext(options.get('path'))
related = Sound.objects.filter(path__icontains = path).first() related = Sound.objects.filter(path__icontains = path).first()
@ -143,12 +131,12 @@ class Command (BaseCommand):
if options.get('diffusion') and related.diffusion: if options.get('diffusion') and related.diffusion:
related = related.diffusion related = related.diffusion
importer = Importer(related = related, path = path) importer = Importer(related = related, path = path, save = True)
for track in importer.tracks: for track in importer.tracks:
logger.log('imported track at {pos}: {name}, by ' logger.info('imported track at {pos}: {title}, by '
'{artist}'.format( '{artist}'.format(
pos = track.pos, pos = track.position,
name = track.name, artist = track.artist title = track.title, artist = track.artist
) )
) )

View File

@ -101,7 +101,7 @@ class SoundInfo:
Get or create a sound using self info. Get or create a sound using self info.
If the sound is created/modified, get its duration and update it If the sound is created/modified, get its duration and update it
(if save is True, sync to DB). (if save is True, sync to DB), and check for a playlist file.
""" """
sound, created = Sound.objects.get_or_create( sound, created = Sound.objects.get_or_create(
path = self.path, path = self.path,
@ -116,6 +116,23 @@ class SoundInfo:
self.sound = sound self.sound = sound
return sound return sound
def find_playlist(self, sound):
"""
Find a playlist file corresponding to the sound path
"""
import aircox.programs.management.commands.import_playlist \
as import_playlist
path = os.path.splitext(self.sound.path)[0] + '.csv'
if not os.path.exists(path):
return
old = Tracks.get_for(object = sound).exclude(tracks_id)
if old:
return
import_playlist.Importer(sound, path, save=True)
def find_diffusion(self, program, save = True): def find_diffusion(self, program, save = True):
""" """
For a given program, check if there is an initial diffusion For a given program, check if there is an initial diffusion
@ -229,7 +246,6 @@ class Command(BaseCommand):
'and react in consequence' 'and react in consequence'
) )
def handle(self, *args, **options): def handle(self, *args, **options):
if options.get('scan'): if options.get('scan'):
self.scan() self.scan()
@ -287,6 +303,7 @@ class Command(BaseCommand):
si = SoundInfo(path) si = SoundInfo(path)
si.get_sound(sound_kwargs, True) si.get_sound(sound_kwargs, True)
si.find_diffusion(program) si.find_diffusion(program)
si.find_playlist(si.sound)
sounds.append(si.sound.pk) sounds.append(si.sound.pk)
# sounds in db & unchecked # sounds in db & unchecked

View File

@ -723,7 +723,7 @@ class Diffusion(models.Model):
) )
class Track(Nameable,Related): class Track(Related):
""" """
Track of a playlist of an object. The position can either be expressed 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. as the position in the playlist or as the moment in seconds it started.
@ -731,6 +731,10 @@ class Track(Nameable,Related):
# There are no nice solution for M2M relations ship (even without # There are no nice solution for M2M relations ship (even without
# through) in django-admin. So we unfortunately need to make one- # through) in django-admin. So we unfortunately need to make one-
# to-one relations and add a position argument # to-one relations and add a position argument
title = models.CharField (
_('title'),
max_length = 128,
)
artist = models.CharField( artist = models.CharField(
_('artist'), _('artist'),
max_length = 128, max_length = 128,
@ -757,7 +761,7 @@ class Track(Nameable,Related):
) )
def __str__(self): def __str__(self):
return ' '.join([self.artist, ':', self.name ]) return '{self.artist} -- {self.title}'.format(self=self)
class Meta: class Meta:
verbose_name = _('Track') verbose_name = _('Track')

View File

@ -46,18 +46,10 @@ ensure(
ensure('AIRCOX_SCHEDULED_STREAM', 0) 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 # Import playlist: columns for CSV file
ensure( ensure(
'AIRCOX_IMPORT_PLAYLIST_CSV_COLS', 'AIRCOX_IMPORT_PLAYLIST_CSV_COLS',
('artist', 'title', 'minutes', 'seconds', 'tags', 'version') ('artist', 'title', 'minutes', 'seconds', 'tags', 'info')
) )
# Import playlist: column delimiter of csv text files # Import playlist: column delimiter of csv text files
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';') ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';')

View File

@ -9,7 +9,7 @@ import aircox.programs.models as programs
class TrackForm (forms.ModelForm): class TrackForm (forms.ModelForm):
class Meta: class Meta:
model = programs.Track model = programs.Track
fields = ['artist', 'name', 'tags', 'position'] fields = ['artist', 'title', 'tags', 'position']
widgets = { widgets = {
# 'artist': al.TextWidget('TrackArtistAutocomplete'), # 'artist': al.TextWidget('TrackArtistAutocomplete'),
# 'name': al.TextWidget('TrackNameAutocomplete'), # 'name': al.TextWidget('TrackNameAutocomplete'),

View File

@ -231,7 +231,7 @@ class Playlist(sections.List):
def get_object_list(self): def get_object_list(self):
tracks = programs.Track.get_for(object = self.object.related) \ tracks = programs.Track.get_for(object = self.object.related) \
.order_by('position') .order_by('position')
return [ sections.ListItem(title=track.name, content=track.artist) return [ sections.ListItem(title=track.title, content=track.artist)
for track in tracks ] for track in tracks ]