WIP - Sound.file instead of Sound.path; fix issues with player; program.path is now relative

This commit is contained in:
bkfox 2022-03-18 14:12:59 +01:00
parent d17d6831dd
commit e3b744be70
17 changed files with 109 additions and 118 deletions

View File

@ -28,7 +28,7 @@ class SoundInline(admin.TabularInline):
max_num = 0
def audio(self, obj):
return mark_safe('<audio src="{}" controls></audio>'.format(obj.url()))
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
audio.short_descripton = _('Audio')
def get_queryset(self, request):
@ -46,10 +46,10 @@ class SoundAdmin(admin.ModelAdmin):
search_fields = ['name', 'program__title']
fieldsets = [
(None, {'fields': ['name', 'path', 'type', 'program', 'episode']}),
(None, {'fields': ['name', 'file', 'type', 'program', 'episode']}),
(None, {'fields': ['duration', 'is_public', 'is_good_quality', 'mtime']}),
]
readonly_fields = ('path', 'duration',)
readonly_fields = ('file', 'duration',)
inlines = [SoundTrackInline]
def related(self, obj):
@ -59,7 +59,7 @@ class SoundAdmin(admin.ModelAdmin):
related.short_description = _('Program / Episode')
def audio(self, obj):
return mark_safe('<audio src="{}" controls></audio>'.format(obj.url()))
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
audio.short_descripton = _('Audio')

View File

@ -40,7 +40,7 @@ class AdminSite(admin.AdminSite):
return context
def get_urls(self):
urls = super().get_urls() + [
urls = [
path('api/', include((self.router.urls, 'api'))),
path('tools/statistics/',
self.admin_view(StatisticsView.as_view()),
@ -48,7 +48,7 @@ class AdminSite(admin.AdminSite):
path('tools/statistics/<date:date>/',
self.admin_view(StatisticsView.as_view()),
name='tools-stats'),
] + self.extra_urls
] + self.extra_urls + super().get_urls()
return urls
def get_tools(self):

View File

@ -128,7 +128,7 @@ class Command(BaseCommand):
def handle(self, path, *args, **options):
# FIXME: absolute/relative path of sounds vs given path
if options.get('sound'):
sound = Sound.objects.filter(path__icontains=options.get('sound'))\
sound = Sound.objects.filter(file__icontains=options.get('sound'))\
.first()
else:
path_, ext = os.path.splitext(path)

View File

@ -36,6 +36,7 @@ import mutagen
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler, FileModifiedEvent
from django.conf import settings as conf
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone as tz
from django.utils.translation import gettext as _
@ -64,12 +65,16 @@ class SoundFile:
def __init__(self, path):
self.path = path
@property
def sound_path(self):
return self.path.replace(conf.MEDIA_ROOT + '/', '')
def sync(self, sound=None, program=None, deleted=False, **kwargs):
"""
Update related sound model and save it.
"""
if deleted:
sound = Sound.objects.filter(path=self.path).first()
sound = Sound.objects.filter(file=self.path).first()
if sound:
sound.type = sound.TYPE_REMOVED
sound.check_on_file()
@ -78,13 +83,13 @@ class SoundFile:
# FIXME: sound.program as not null
program = kwargs['program'] = Program.get_from_path(self.path)
sound, created = Sound.objects.get_or_create(path=self.path, defaults=kwargs) \
sound, created = Sound.objects.get_or_create(file=self.sound_path, defaults=kwargs) \
if not sound else (sound, False)
self.sound = sound
sound.program = program
if created or sound.check_on_file():
logger.info('sound is new or have been modified -> %s', self.path)
logger.info('sound is new or have been modified -> %s', self.sound_path)
self.read_path()
sound.name = self.path_info.get('name')
@ -153,7 +158,7 @@ class SoundFile:
if not diffusion:
return None
logger.info('%s <--> %s', self.sound.path, str(diffusion.episode))
logger.info('%s <--> %s', self.sound.file.name, str(diffusion.episode))
self.sound.episode = diffusion.episode
return diffusion
@ -172,7 +177,7 @@ class SoundFile:
return
# import playlist
path = os.path.splitext(self.sound.path)[0] + '.csv'
path = os.path.splitext(self.sound.file.path)[0] + '.csv'
if os.path.exists(path):
PlaylistImport(path, sound=sound).run()
# use metadata
@ -227,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler):
def on_moved(self, event):
logger.info('sound moved: %s -> %s', event.src_path, event.dest_path)
def moved(event, sound_kwargs):
sound = Sound.objects.filter(path=event.src_path)
sound = Sound.objects.filter(file=event.src_path)
sound_file = SoundFile(event.dest_path) if not sound else sound
sound_file.sync(**sound_kwargs)
self.pool.submit(moved, event, self.sound_kwargs)
@ -268,7 +273,8 @@ class Command(BaseCommand):
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
type=Sound.TYPE_EXCERPT,
)
dirs.append(os.path.join(program.path))
dirs.append(os.path.join(program.abspath))
return dirs
def scan_for_program(self, program, subdir, **sound_kwargs):
"""
@ -279,7 +285,7 @@ class Command(BaseCommand):
if not program.ensure_dir(subdir):
return
subdir = os.path.join(program.path, subdir)
subdir = os.path.join(program.abspath, subdir)
sounds = []
# sounds in directory
@ -293,7 +299,7 @@ class Command(BaseCommand):
sounds.append(sound_file.sound.pk)
# sounds in db & unchecked
sounds = Sound.objects.filter(path__startswith=subdir). \
sounds = Sound.objects.filter(file__startswith=subdir). \
exclude(pk__in=sounds)
self.check_sounds(sounds, program=program)
@ -302,7 +308,7 @@ class Command(BaseCommand):
# check files
for sound in qs:
if sound.check_on_file():
SoundFile(sound.path).sync(sound=sound, **sync_kwargs)
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
def monitor(self):
""" Run in monitor mode """

View File

@ -160,7 +160,7 @@ class Command (BaseCommand):
self.bad = []
self.good = []
for sound in self.sounds:
logger.info('analyse ' + sound.path)
logger.info('analyse ' + sound.file.name)
sound.analyse()
sound.check(attr, minmax[0], minmax[1])
if sound.bad:
@ -171,6 +171,6 @@ class Command (BaseCommand):
# resume
if options.get('resume'):
for sound in self.good:
logger.info('\033[92m+ %s\033[0m', sound.path)
logger.info('\033[92m+ %s\033[0m', sound.file.name)
for sound in self.bad:
logger.info('\033[91m+ %s\033[0m', sound.path)
logger.info('\033[91m+ %s\033[0m', sound.file.name)

View File

@ -7,6 +7,7 @@ import os
import shutil
import pytz
from django.conf import settings as conf
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Q
@ -73,14 +74,17 @@ class Program(Page):
self.slug.replace('-', '_'))
@property
def archives_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
def abspath(self):
""" Return absolute path to program's dir """
return os.path.join(conf.MEDIA_ROOT, self.path)
@property
def excerpts_path(self):
return os.path.join(
self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
)
def archives_path(self, abs=False):
return os.path.join(abs and self.abspath or self.path,
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
def excerpts_path(self, abs=False):
return os.path.join(abs and self.abspath or self.path,
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs)
@ -94,6 +98,8 @@ class Program(Page):
Return a Program from the given path. We assume the path has been
given in a previous time by this model (Program.path getter).
"""
if path.startswith(conf.MEDIA_ROOT):
path = path.replace(conf.MEDIA_ROOT + '/', '')
path = path.replace(settings.AIRCOX_PROGRAMS_DIR, '')
while path[0] == '/':
@ -107,10 +113,9 @@ class Program(Page):
Make sur the program's dir exists (and optionally subdir). Return True
if the dir (or subdir) exists.
"""
path = os.path.join(self.path, subdir) if subdir else \
self.path
path = os.path.join(self.abspath, subdir) if subdir else \
self.abspath
os.makedirs(path, exist_ok=True)
return os.path.exists(path)
class Meta:
@ -127,14 +132,15 @@ class Program(Page):
# TODO: move in signals
path_ = getattr(self, '__initial_path', None)
abspath = os.path.join(conf.MEDIA_ROOT, path_)
if path_ is not None and path_ != self.path and \
os.path.exists(path_) and not os.path.exists(self.path):
os.path.exists(abspath) and not os.path.exists(self.abspath):
logger.info('program #%s\'s dir changed to %s - update it.',
self.id, self.title)
shutil.move(path_, self.path)
shutil.move(abspath, self.abspath)
Sound.objects.filter(path__startswith=path_) \
.update(path=Concat('path', Substr(F('path'), len(path_))))
.update(file=Concat('file', Substr(F('file'), len(path_))))
class ProgramChildQuerySet(PageQuerySet):

View File

@ -4,7 +4,8 @@ import os
from django.conf import settings as main_settings
from django.db import models
from django.db.models import Q
from django.db.models import Q, Value as V
from django.db.models.functions import Concat
from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
@ -54,7 +55,9 @@ class SoundQuerySet(models.QuerySet):
self = self.archive()
if order_by:
self = self.order_by('path')
return self.filter(path__isnull=False).values_list('path', flat=True)
return self.filter(file__isnull=False) \
.annotate(file_path=Concat(V(conf.MEDIA_ROOT), 'file')) \
.values_list('file_path', flat=True)
def search(self, query):
return self.filter(
@ -94,21 +97,15 @@ class Sound(models.Model):
position = models.PositiveSmallIntegerField(
_('order'), default=0, help_text=_('position in the playlist'),
)
# FIXME: url() does not use the same directory than here
# should we use FileField for more reliability?
path = models.FilePathField(
_('file'),
path=settings.AIRCOX_PROGRAMS_DIR,
match=r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT)
.replace('.', r'\.') + ')$',
recursive=True, max_length=255,
blank=True, null=True, unique=True,
def _upload_to(self, filename):
subdir = AIRCOX_SOUND_ARCHIVES_SUBDIR if self.type == self.TYPE_ARCHIVE else \
AIRCOX_SOUND_EXCERPTS_SUBDIR
return os.path.join(o.program.path, subdir, filename)
file = models.FileField(
_('file'), upload_to=_upload_to
)
#embed = models.TextField(
# _('embed'),
# blank=True, null=True,
# help_text=_('HTML code to embed a sound from an external plateform'),
#)
duration = models.TimeField(
_('duration'),
blank=True, null=True,
@ -134,8 +131,12 @@ class Sound(models.Model):
verbose_name = _('Sound')
verbose_name_plural = _('Sounds')
@property
def url(self):
return self.file and self.file.url
def __str__(self):
return '/'.join(self.path.split('/')[-3:])
return '/'.join(self.file.path.split('/')[-3:])
def save(self, check=True, *args, **kwargs):
if self.episode is not None and self.program is None:
@ -145,17 +146,12 @@ class Sound(models.Model):
self.__check_name()
super().save(*args, **kwargs)
def url(self):
""" Return an url to the file. """
path = self.path.replace(main_settings.MEDIA_ROOT, '', 1)
return (main_settings.MEDIA_URL + path).replace('//','/')
# TODO: rename get_file_mtime(self)
def get_mtime(self):
"""
Get the last modification date from file
"""
mtime = os.stat(self.path).st_mtime
mtime = os.stat(self.file.path).st_mtime
mtime = tz.datetime.fromtimestamp(mtime)
mtime = mtime.replace(microsecond=0)
return tz.make_aware(mtime, tz.get_current_timezone())
@ -163,7 +159,7 @@ class Sound(models.Model):
def file_exists(self):
""" Return true if the file still exists. """
return os.path.exists(self.path)
return os.path.exists(self.file.path)
def check_on_file(self):
"""
@ -173,7 +169,7 @@ class Sound(models.Model):
if not self.file_exists():
if self.type == self.TYPE_REMOVED:
return
logger.info('sound %s: has been removed', self.path)
logger.info('sound %s: has been removed', self.file.name)
self.type = self.TYPE_REMOVED
return True
@ -183,7 +179,7 @@ class Sound(models.Model):
if self.type == self.TYPE_REMOVED and self.program:
changed = True
self.type = self.TYPE_ARCHIVE \
if self.path.startswith(self.program.archives_path) else \
if self.file.path.startswith(self.program.archives_path) else \
self.TYPE_EXCERPT
# check mtime -> reset quality if changed (assume file changed)
@ -193,15 +189,15 @@ class Sound(models.Model):
self.mtime = mtime
self.is_good_quality = None
logger.info('sound %s: m_time has changed. Reset quality info',
self.path)
self.file.name)
return True
return changed
def __check_name(self):
if not self.name and self.path:
if not self.name and self.file and self.file.path:
# FIXME: later, remove date?
self.name = os.path.basename(self.path)
self.name = os.path.basename(self.file.name)
self.name = os.path.splitext(self.name)[0]
self.name = self.name.replace('_', ' ')

View File

@ -55,7 +55,6 @@ class LogInfoSerializer(serializers.Serializer):
class SoundSerializer(serializers.ModelSerializer):
# serializers.HyperlinkedIdentityField(view_name='sound', format='html')
class Meta:
model = Sound
fields = ['pk', 'name', 'program', 'episode', 'type',
@ -64,7 +63,7 @@ class SoundSerializer(serializers.ModelSerializer):
def get_field_names(self, *args):
names = super().get_field_names(*args)
if 'request' in self.context and self.context['request'].user.is_staff:
names.append('path')
names.append('file')
return names
class PodcastSerializer(serializers.ModelSerializer):

View File

@ -87,8 +87,7 @@ ensure('AIRCOX_DEFAULT_USER_GROUPS', {
# Directory for the programs data
# TODO: rename to PROGRAMS_ROOT
ensure('AIRCOX_PROGRAMS_DIR',
os.path.join(settings.MEDIA_ROOT, 'programs'))
ensure('AIRCOX_PROGRAMS_DIR', 'programs')
########################################################################
@ -152,8 +151,3 @@ ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';')
ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"')
if settings.MEDIA_ROOT not in AIRCOX_PROGRAMS_DIR:
# PROGRAMS_DIR must be in MEDIA_ROOT for easy files url resolution
# later should this restriction disappear.
raise ValueError("settings: AIRCOX_PROGRAMS_DIR must be in MEDIA_ROOT")

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ List item for a podcast.
{% if object.embed %}
{{ object.embed|safe }}
{% else %}
<audio src="{{ object.url }}" controls>
<audio src="{{ object.file.url }}" controls>
{% endif %}
{% endcomment %}
<a-sound-item :data="{{ object|json }}" :player="player"

View File

@ -187,5 +187,5 @@ class QueueSourceViewSet(SourceViewSet):
sound = get_object_or_404(self.get_sound_queryset(),
pk=request.data['sound_id'])
return self._run(
pk, lambda s: s.push(sound.path) if sound.path else None)
pk, lambda s: s.push(sound.file.path) if sound.file.path else None)

View File

@ -12,6 +12,7 @@ const App = {
export const PlayerApp = {
el: '#player',
delimiters: ['[[', ']]'],
components: {...components},
}

View File

@ -219,7 +219,7 @@ export default {
/// Push items to playlist (by name)
push(playlist, ...items) {
return this.$refs[playlist].push(...items);
return this.sets[playlist].push(...items);
},
/// Push and play items

View File

@ -6,7 +6,7 @@
:key="index">
<a :class="index == selectedIndex ? 'is-active' : ''">
<ASoundItem
:data="item" :index="index" :player="player" :set="set"
:data="item" :index="index" :set="set" :player="player_"
@togglePlay="togglePlay(index)"
:actions="actions">
<template v-slot:actions="{}">
@ -38,7 +38,8 @@ export default {
},
computed: {
self() { return this; }
self() { return this; },
player_() { return this.player || window.aircox.player },
},
methods: {
@ -50,8 +51,8 @@ export default {
},
togglePlay(index) {
if(this.player.isPlaying(this.set.get(index)))
this.player.pause();
if(this.player_.isPlaying(this.set.get(index)))
this.player_.pause();
else
this.select(index)
},

View File

@ -35,6 +35,11 @@ window.aircox = {
init(props=null, {config=null, builder=null, initBuilder=true,
initPlayer=true, hotReload=false}={})
{
if(initPlayer) {
let playerBuilder = this.playerBuilder
playerBuilder.mount()
}
if(initBuilder) {
builder = builder || this.builder
this.builder = builder
@ -46,11 +51,6 @@ window.aircox = {
if(hotReload)
builder.enableHotReload(hotReload)
}
if(initPlayer) {
let playerBuilder = this.playerBuilder
playerBuilder.mount()
}
},
}

View File

@ -1,31 +1,19 @@
asgiref==3.2.10
bleach>=3.3.0
Django==3.1.6
django-admin-sortable2==0.7.7
django-ckeditor==6.0.0
django-content-editor==3.0.6
django-filer==2.0.2
django-filter==2.3.0
django-honeypot==0.9.0
django-js-asset==1.2.2
django-model-utils==4.0.0
django-mptt==0.11.0
django-polymorphic==3.0.0
django-taggit==1.3.0
djangorestframework==3.11.2
easy-thumbnails==2.7
gunicorn==20.0.4
mutagen==1.45.1
packaging==20.4
pathtools==0.1.2
Pillow==8.1.1
psutil==5.7.2
pyparsing==2.4.7
pytz==2020.1
Django~=3.0
djangorestframework~=3.13
django-model-utils>=4.2
django-filter~=21.1
django-filer~=2.1
django-honeypot~=1.0
django-taggit~=2.1
django-admin-sortable2~=1.0
django-ckeditor~=6.2
bleach~=4.1
easy-thumbnails~=2.8
tzlocal~=4.1
mutagen~=1.45
Pillow~=9.0
psutil~=5.9
PyYAML==5.4
six==1.14.0
sqlparse==0.3.1
tzlocal==2.1
Unidecode==1.1.1
watchdog==0.10.3
webencodings==0.5.1
watchdog~=2.1