From 32a30004d6fc3e8425adba201d444b0b68f553cb Mon Sep 17 00:00:00 2001 From: bkfox Date: Sat, 16 Jul 2016 23:51:53 +0200 Subject: [PATCH] playlist import -- fixes and integration into the sound monitor --- programs/admin.py | 6 +- .../management/commands/import_playlist.py | 96 ++++++++----------- .../management/commands/sounds_monitor.py | 21 +++- programs/models.py | 8 +- programs/settings.py | 10 +- website/forms.py | 2 +- website/sections.py | 2 +- 7 files changed, 75 insertions(+), 70 deletions(-) diff --git a/programs/admin.py b/programs/admin.py index a1c4b47..057d847 100755 --- a/programs/admin.py +++ b/programs/admin.py @@ -166,5 +166,9 @@ class ScheduleAdmin(admin.ModelAdmin): list_display = ['id', 'program_name', 'frequency', 'date', 'day', 'rerun'] 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'] + diff --git a/programs/management/commands/import_playlist.py b/programs/management/commands/import_playlist.py index 9b95be5..a150b6f 100644 --- a/programs/management/commands/import_playlist.py +++ b/programs/management/commands/import_playlist.py @@ -2,20 +2,13 @@ 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} +Playlists are in CSV format, 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 @@ -23,52 +16,41 @@ import logging from argparse import RawTextHelpFormatter from django.core.management.base import BaseCommand, CommandError +from django.contrib.contenttypes.models import ContentType from aircox.programs.models import * import aircox.programs.settings as settings -__doc__ = __doc__.format(settings) +__doc__ = __doc__.format(settings = settings) logger = logging.getLogger('aircox.tools') class Importer: - type = None data = None tracks = None - def __init__(self, related = None, path = None): + def __init__(self, related = None, path = None, save = False): if path: self.read(path) if related: - self.make_playlist(related, True) + self.make_playlist(related, save) 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() - ] + self.data = list(csv.reader( + file, + delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER, + quotechar = settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE, + )) 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: return default index = maps.index(field) @@ -79,26 +61,30 @@ class Importer: 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 + maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS tracks = [] + pos_in_secs = ('minutes' or 'seconds') in maps 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 + position = \ + int(self.__get(line, 'minutes', 0)) * 60 + \ + int(self.__get(line, 'seconds', 0)) \ + if pos_in_secs else index - kwargs['related'] = related - kwargs.update({ - k: self.__get(line, k) for k in maps - if k not in ('minutes', 'seconds') - }) + track, created = Track.objects.get_or_create( + related_type = ContentType.objects.get_for_model(related), + related_id = related.pk, + 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: track.save() tracks.append(track) @@ -114,7 +100,7 @@ class Command (BaseCommand): now = tz.datetime.today() parser.add_argument( - 'path', type=str, + 'path', metavar='PATH', type=str, help='path of the input playlist to read' ) parser.add_argument( @@ -130,7 +116,9 @@ class Command (BaseCommand): 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() + related = Sound.objects.filter( + path__icontains = options.get('sound') + ).first() else: path, ext = os.path.splitext(options.get('path')) related = Sound.objects.filter(path__icontains = path).first() @@ -143,12 +131,12 @@ class Command (BaseCommand): if options.get('diffusion') and related.diffusion: related = related.diffusion - importer = Importer(related = related, path = path) + importer = Importer(related = related, path = path, save = True) for track in importer.tracks: - logger.log('imported track at {pos}: {name}, by ' - '{artist}'.format( - pos = track.pos, - name = track.name, artist = track.artist + logger.info('imported track at {pos}: {title}, by ' + '{artist}'.format( + pos = track.position, + title = track.title, artist = track.artist ) ) diff --git a/programs/management/commands/sounds_monitor.py b/programs/management/commands/sounds_monitor.py index 57b92ed..1b16a9a 100644 --- a/programs/management/commands/sounds_monitor.py +++ b/programs/management/commands/sounds_monitor.py @@ -101,7 +101,7 @@ class SoundInfo: Get or create a sound using self info. 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( path = self.path, @@ -116,6 +116,23 @@ class SoundInfo: self.sound = 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): """ For a given program, check if there is an initial diffusion @@ -229,7 +246,6 @@ class Command(BaseCommand): 'and react in consequence' ) - def handle(self, *args, **options): if options.get('scan'): self.scan() @@ -287,6 +303,7 @@ class Command(BaseCommand): si = SoundInfo(path) si.get_sound(sound_kwargs, True) si.find_diffusion(program) + si.find_playlist(si.sound) sounds.append(si.sound.pk) # sounds in db & unchecked diff --git a/programs/models.py b/programs/models.py index f859987..ea93bd1 100755 --- a/programs/models.py +++ b/programs/models.py @@ -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 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 # through) in django-admin. So we unfortunately need to make one- # to-one relations and add a position argument + title = models.CharField ( + _('title'), + max_length = 128, + ) artist = models.CharField( _('artist'), max_length = 128, @@ -757,7 +761,7 @@ class Track(Nameable,Related): ) def __str__(self): - return ' '.join([self.artist, ':', self.name ]) + return '{self.artist} -- {self.title}'.format(self=self) class Meta: verbose_name = _('Track') diff --git a/programs/settings.py b/programs/settings.py index dc1e984..e2467b0 100755 --- a/programs/settings.py +++ b/programs/settings.py @@ -46,18 +46,10 @@ ensure( 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') + ('artist', 'title', 'minutes', 'seconds', 'tags', 'info') ) # Import playlist: column delimiter of csv text files ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';') diff --git a/website/forms.py b/website/forms.py index f632eee..67ea16f 100644 --- a/website/forms.py +++ b/website/forms.py @@ -9,7 +9,7 @@ import aircox.programs.models as programs class TrackForm (forms.ModelForm): class Meta: model = programs.Track - fields = ['artist', 'name', 'tags', 'position'] + fields = ['artist', 'title', 'tags', 'position'] widgets = { # 'artist': al.TextWidget('TrackArtistAutocomplete'), # 'name': al.TextWidget('TrackNameAutocomplete'), diff --git a/website/sections.py b/website/sections.py index f04a2dd..56b3224 100644 --- a/website/sections.py +++ b/website/sections.py @@ -231,7 +231,7 @@ class Playlist(sections.List): def get_object_list(self): tracks = programs.Track.get_for(object = self.object.related) \ .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 ]