playlist import -- fixes and integration into the sound monitor
This commit is contained in:
parent
161af3fb1a
commit
32a30004d6
|
@ -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']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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':
|
|
||||||
self.type = 'csv'
|
|
||||||
self.data = csv.reader(
|
|
||||||
file,
|
file,
|
||||||
delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
|
delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
|
||||||
quotechar = settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
|
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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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', ';')
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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 ]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user