diff --git a/aircox/controllers/sound_file.py b/aircox/controllers/sound_file.py index ef30944..2cf8821 100644 --- a/aircox/controllers/sound_file.py +++ b/aircox/controllers/sound_file.py @@ -72,10 +72,10 @@ class SoundFile: sound.save() if not sound.episodesound_set.all().exists(): - self.find_episode_sound(sound) + self.create_episode_sound(sound) return sound - def find_episode_sound(self, sound): + def create_episode_sound(self, sound): episode = sound.find_episode() if episode: # FIXME: position from name diff --git a/aircox/models/episode.py b/aircox/models/episode.py index ea7b4f3..b7e666b 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -1,5 +1,6 @@ import os +from django.conf import settings as d_settings from django.db import models from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ @@ -105,12 +106,11 @@ class EpisodeSoundQuerySet(models.QuerySet): return self.available().filter(broadcast=True) def playlist(self, order="position"): + # TODO: subquery expression if order: self = self.order_by(order) - return [ - os.path.join(settings.MEDIA_ROOT, file) - for file in self.filter(file__isnull=False, is_removed=False).Values_list("file", flat=True) - ] + query = self.filter(sound__file__isnull=False, sound__is_removed=False).values_list("sound__file", flat=True) + return [os.path.join(d_settings.MEDIA_ROOT, file) for file in query] class EpisodeSound(models.Model): diff --git a/aircox/models/signals.py b/aircox/models/signals.py index e2380a8..b9d5dd4 100755 --- a/aircox/models/signals.py +++ b/aircox/models/signals.py @@ -91,12 +91,12 @@ def schedule_post_save(sender, instance, created, *args, **kwargs): def schedule_pre_delete(sender, instance, *args, **kwargs): """Delete later corresponding diffusion to a changed schedule.""" Diffusion.objects.filter(schedule=instance).after(tz.now()).delete() - Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete() + Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete() @receiver(signals.post_delete, sender=Diffusion) def diffusion_post_delete(sender, instance, *args, **kwargs): - Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete() + Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete() @receiver(signals.post_delete, sender=Sound) diff --git a/aircox/models/sound.py b/aircox/models/sound.py index afe032c..736fb3b 100644 --- a/aircox/models/sound.py +++ b/aircox/models/sound.py @@ -26,16 +26,11 @@ class SoundQuerySet(FileQuerySet): """Return sounds that are archives.""" return self.filter(broadcast=True) - def playlist(self, broadcast=True, order_by=True): + def playlist(self, order_by="file"): """Return files absolute paths as a flat list (exclude sound without - path). - - If `order_by` is True, order by path. - """ - if broadcast: - self = self.broadcast() + path).""" if order_by: - self = self.order_by("file") + self = self.order_by(order_by) return [ os.path.join(conf.MEDIA_ROOT, file) for file in self.filter(file__isnull=False).values_list("file", flat=True) @@ -66,6 +61,8 @@ class Sound(File): help_text=_("The sound is broadcasted on air"), ) + objects = SoundQuerySet.as_manager() + class Meta: verbose_name = _("Sound file") verbose_name_plural = _("Sound files") diff --git a/aircox/tests/conftest.py b/aircox/tests/conftest.py index 3284dee..e5afcaf 100644 --- a/aircox/tests/conftest.py +++ b/aircox/tests/conftest.py @@ -131,25 +131,32 @@ def episode(episodes): @pytest.fixture -def podcasts(episodes): - items = [] - for episode in episodes: - sounds = baker.prepare( - models.Sound, - episode=episode, - program=episode.program, - is_public=True, - _quantity=2, - ) - for i, sound in enumerate(sounds): - sound.file = f"test_sound_{episode.pk}_{i}.mp3" - items += sounds - return items +def sound(program): + return baker.make(models.Sound, file="tmp/test.wav", program=program) @pytest.fixture -def sound(program): - return baker.make(models.Sound, file="tmp/test.wav", program=program) +def sounds(program): + objs = [ + models.Sound(program=program, file=f"tmp/test-{i}.wav", broadcast=(i == 0), is_downloadable=(i == 1)) + for i in range(0, 3) + ] + models.Sound.objects.bulk_create(objs) + return objs + + +@pytest.fixture +def podcasts(episode, sounds): + objs = [ + models.EpisodeSound( + episode=episode, + sound=sound, + broadcast=True, + ) + for sound in sounds + ] + models.EpisodeSound.objects.bulk_create(objs) + return objs @pytest.fixture diff --git a/aircox/tests/controllers/test_sound_file.py b/aircox/tests/controllers/test_sound_file.py index 9e5bcc8..cb9b033 100644 --- a/aircox/tests/controllers/test_sound_file.py +++ b/aircox/tests/controllers/test_sound_file.py @@ -1,14 +1,12 @@ import pytest -from datetime import timedelta from django.conf import settings as conf -from django.utils import timezone as tz -from aircox import models from aircox.controllers.sound_file import SoundFile +# FIXME: use from tests.models.sound @pytest.fixture def path_infos(): return { @@ -27,6 +25,7 @@ def path_infos(): "day": 2, "hour": 10, "minute": 13, + "n": None, "name": "Sample 2", }, "test/20220103_1_sample_3.mp3": { @@ -56,42 +55,25 @@ def sound_files(path_infos): return {k: r for k, r in ((path, SoundFile(conf.MEDIA_ROOT + "/" + path)) for path in path_infos.keys())} +@pytest.fixture +def sound_file(sound_files): + return next(sound_files.items()) + + def test_sound_path(sound_files): for path, sound_file in sound_files.items(): assert path == sound_file.sound_path -def test_read_path(path_infos, sound_files): - for path, sound_file in sound_files.items(): - expected = path_infos[path] - result = sound_file.read_path(path) - # remove None values - result = {k: v for k, v in result.items() if v is not None} - assert expected == result, "path: {}".format(path) +class TestSoundFile: + def sound_path(self, sound_file): + assert sound_file[0] == sound_file[1].sound_path + def sync(self): + raise NotImplementedError("test is not implemented") -def _setup_diff(program, info): - episode = models.Episode(program=program, title="test-episode") - at = tz.datetime(**{k: info[k] for k in ("year", "month", "day", "hour", "minute") if info.get(k)}) - at = tz.make_aware(at) - diff = models.Diffusion(episode=episode, start=at, end=at + timedelta(hours=1)) - episode.save() - diff.save() - return diff + def create_episode_sound(self): + raise NotImplementedError("test is not implemented") - -@pytest.mark.django_db(transaction=True) -def test_find_episode(sound_files): - station = models.Station(name="test-station") - program = models.Program(station=station, title="test") - station.save() - program.save() - - for path, sound_file in sound_files.items(): - infos = sound_file.read_path(path) - diff = _setup_diff(program, infos) - sound = models.Sound(program=diff.program, file=path) - result = sound_file.find_episode(sound, infos) - assert diff.episode == result - - # TODO: find_playlist, sync + def _on_delete(self): + raise NotImplementedError("test is not implemented") diff --git a/aircox/tests/controllers/test_sound_monitor.py b/aircox/tests/controllers/test_sound_monitor.py index 0913a63..d1c5765 100644 --- a/aircox/tests/controllers/test_sound_monitor.py +++ b/aircox/tests/controllers/test_sound_monitor.py @@ -223,22 +223,19 @@ class TestSoundMonitor: [ (("scan all programs...",), {}), ] - + [ - ((f"#{program.id} {program.title}",), {}) - for program in programs - ] + + [((f"#{program.id} {program.title}",), {}) for program in programs] ) assert dirs == [program.abspath for program in programs] traces = tuple( [ [ ( - (program, settings.SOUND_ARCHIVES_SUBDIR), - {"logger": logger, "type": Sound.TYPE_ARCHIVE}, + (program, settings.SOUND_BROADCASTS_SUBDIR), + {"logger": logger, "broadcast": True}, ), ( (program, settings.SOUND_EXCERPTS_SUBDIR), - {"logger": logger, "type": Sound.TYPE_EXCERPT}, + {"logger": logger, "broadcast": False}, ), ] for program in programs @@ -247,6 +244,7 @@ class TestSoundMonitor: traces_flat = tuple([item for sublist in traces for item in sublist]) assert interface._traces("scan_for_program") == traces_flat + # TODO / FIXME def broken_test_monitor(self, monitor, monitor_interfaces, logger): def sleep(*args, **kwargs): monitor.stop() @@ -260,6 +258,7 @@ class TestSoundMonitor: assert observer schedules = observer._traces("schedule") for (handler, *_), kwargs in schedules: + breakpoint() assert isinstance(handler, sound_monitor.MonitorHandler) assert isinstance(handler.pool, futures.ThreadPoolExecutor) assert (handler.subdir, handler.type) in ( diff --git a/aircox/tests/models/test_sound.py b/aircox/tests/models/test_sound.py new file mode 100644 index 0000000..01052f0 --- /dev/null +++ b/aircox/tests/models/test_sound.py @@ -0,0 +1,122 @@ +from datetime import timedelta +import os +import pytest + +from django.conf import settings +from django.utils import timezone as tz + +from aircox import models + + +@pytest.fixture +def path_infos(): + return { + "test/20220101_10h13_1_sample_1.mp3": { + "year": 2022, + "month": 1, + "day": 1, + "hour": 10, + "minute": 13, + "n": 1, + "name": "Sample 1", + }, + "test/20220102_10h13_sample_2.mp3": { + "year": 2022, + "month": 1, + "day": 2, + "hour": 10, + "minute": 13, + "n": None, + "name": "Sample 2", + }, + "test/20220103_1_sample_3.mp3": { + "year": 2022, + "month": 1, + "day": 3, + "hour": None, + "minute": None, + "n": 1, + "name": "Sample 3", + }, + "test/20220104_sample_4.mp3": { + "year": 2022, + "month": 1, + "day": 4, + "hour": None, + "minute": None, + "n": None, + "name": "Sample 4", + }, + "test/20220105.mp3": { + "year": 2022, + "month": 1, + "day": 5, + "hour": None, + "minute": None, + "n": None, + "name": "20220105", + }, + } + + +class TestSoundQuerySet: + @pytest.mark.django_db + def test_downloadable(self, sounds): + query = models.Sound.objects.downloadable().values_list("is_downloadable", flat=True) + assert set(query) == {True} + + @pytest.mark.django_db + def test_broadcast(self, sounds): + query = models.Sound.objects.broadcast().values_list("broadcast", flat=True) + assert set(query) == {True} + + @pytest.mark.django_db + def test_playlist(self, sounds): + expected = [os.path.join(settings.MEDIA_ROOT, s.file.path) for s in sounds] + assert models.Sound.objects.all().playlist() == expected + + +class TestSound: + @pytest.mark.django_db + def test_read_path(self, path_infos): + for path, expected in path_infos.items(): + result = models.Sound.read_path(path) + assert expected == result + + @pytest.mark.django_db + def test__as_name(self): + name = "some_1_file" + assert models.Sound._as_name(name) == "Some 1 File" + + def _setup_diff(self, program, info): + episode = models.Episode(program=program, title="test-episode") + at = tz.datetime(**{k: info[k] for k in ("year", "month", "day", "hour", "minute") if info.get(k)}) + at = tz.make_aware(at) + diff = models.Diffusion(episode=episode, start=at, end=at + timedelta(hours=1)) + episode.save() + diff.save() + return diff + + @pytest.mark.django_db(transaction=True) + def test_find_episode(self, program, path_infos): + for path, infos in path_infos.items(): + diff = self._setup_diff(program, infos) + sound = models.Sound(program=diff.program, file=path) + result = sound.find_episode(infos) + assert diff.episode == result + + @pytest.mark.django_db + def test_find_playlist(self): + raise NotImplementedError("test is not implemented") + + @pytest.mark.django_db + def test_get_upload_dir(self): + raise NotImplementedError("test is not implemented") + + @pytest.mark.django_db + def test_sync_fs(self): + raise NotImplementedError("test is not implemented") + + @pytest.mark.django_db + def test_read_metadata(self): + raise NotImplementedError("test is not implemented") diff --git a/aircox/tests/test_program.py b/aircox/tests/test_program.py index 123d90f..c278c60 100644 --- a/aircox/tests/test_program.py +++ b/aircox/tests/test_program.py @@ -1,10 +1,8 @@ +# FIXME: this should be cleaner from itertools import chain import json import pytest from django.urls import reverse -from django.core.files.uploadedfile import SimpleUploadedFile - -from aircox.models import Program @pytest.mark.django_db() @@ -22,20 +20,6 @@ def test_edit_program(user, client, program): assert b"foobar" in response.content -@pytest.mark.django_db() -def test_add_cover(user, client, program, png_content): - assert program.cover is None - user.groups.add(program.editors) - client.force_login(user) - cover = SimpleUploadedFile("cover1.png", png_content, content_type="image/png") - r = client.post( - reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar", "new_cover": cover}, follow=True - ) - assert r.status_code == 200 - p = Program.objects.get(pk=program.pk) - assert "cover1.png" in p.cover.url - - @pytest.mark.django_db() def test_edit_tracklist(user, client, program, episode, tracks): user.groups.add(program.editors) diff --git a/aircox/tests/views/conftest.py b/aircox/tests/views/conftest.py index 44825ac..efb51ac 100644 --- a/aircox/tests/views/conftest.py +++ b/aircox/tests/views/conftest.py @@ -11,6 +11,9 @@ class FakeView: def ___init__(self): self.kwargs = {} + def dispatch(self, *args, **kwargs): + pass + def get(self, *args, **kwargs): pass diff --git a/aircox/tests/views/test_base.py b/aircox/tests/views/test_base.py index 7c05e5a..1e99c3b 100644 --- a/aircox/tests/views/test_base.py +++ b/aircox/tests/views/test_base.py @@ -43,7 +43,6 @@ class TestBaseView: "view": base_view, "station": station, "page": None, # get_page() returns None - "audio_streams": station.streams, "model": base_view.model, } diff --git a/aircox/tests/views/test_mixins.py b/aircox/tests/views/test_mixins.py index ad9d40d..8d8ca60 100644 --- a/aircox/tests/views/test_mixins.py +++ b/aircox/tests/views/test_mixins.py @@ -40,7 +40,7 @@ def parent_mixin(): @pytest.fixture def attach_mixin(): class Mixin(mixins.AttachedToMixin, FakeView): - attach_to_value = models.StaticPage.ATTACH_TO_HOME + attach_to_value = models.StaticPage.Target.HOME return Mixin() @@ -105,10 +105,10 @@ class TestParentMixin: def test_get_parent_not_parent_url_kwargs(self, parent_mixin): assert parent_mixin.get_parent(self.req) is None - def test_get_calls_parent(self, parent_mixin): + def test_dispatch_calls_parent(self, parent_mixin): parent = "parent object" parent_mixin.get_parent = lambda *_, **kw: parent - parent_mixin.get(self.req) + parent_mixin.dispatch(self.req) assert parent_mixin.parent == parent @pytest.mark.django_db @@ -120,7 +120,7 @@ class TestParentMixin: assert set(query) == episodes_id def test_get_context_data_with_parent(self, parent_mixin): - parent_mixin.parent = Interface(cover="parent-cover") + parent_mixin.parent = Interface(cover=Interface(url="parent-cover")) context = parent_mixin.get_context_data() assert context["cover"] == "parent-cover" diff --git a/aircox/views/base.py b/aircox/views/base.py index bb22402..bf9dae7 100644 --- a/aircox/views/base.py +++ b/aircox/views/base.py @@ -7,7 +7,6 @@ __all__ = ("BaseView", "BaseAPIView") class BaseView(TemplateResponseMixin, ContextMixin): - header_template_name = "aircox/widgets/header.html" related_count = 4 related_carousel_count = 8 @@ -50,8 +49,8 @@ class BaseView(TemplateResponseMixin, ContextMixin): return None def get_context_data(self, **kwargs): + kwargs.setdefault("station", self.station) kwargs.setdefault("page", self.get_page()) - kwargs.setdefault("header_template_name", self.header_template_name) if "model" not in kwargs: model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object) diff --git a/aircox_streamer/controllers/monitor.py b/aircox_streamer/controllers/monitor.py index b57822f..34f26d0 100644 --- a/aircox_streamer/controllers/monitor.py +++ b/aircox_streamer/controllers/monitor.py @@ -133,8 +133,10 @@ class Monitor: # get sound diff = None sound = Sound.objects.path(air_uri).first() - if sound and sound.episode_id is not None: - diff = Diffusion.objects.episode(id=sound.episode_id).on_air().now(air_time).first() + if sound: + ids = sound.episodesound_set.values_list("episode_id", flat=True) + if ids: + diff = Diffusion.objects.filter(episode_id__in=ids).on_air().now(air_time).first() # log sound on air return self.log( diff --git a/aircox_streamer/tests/conftest.py b/aircox_streamer/tests/conftest.py index 992daac..dd3cabc 100644 --- a/aircox_streamer/tests/conftest.py +++ b/aircox_streamer/tests/conftest.py @@ -146,24 +146,28 @@ def episode(program): def sound(program, episode): sound = models.Sound( program=program, - episode=episode, name="sound", - type=models.Sound.TYPE_ARCHIVE, - position=0, + broadcast=True, file="sound.mp3", ) - sound.save(check=False) + sound.save(sync=False) return sound +@pytest.fixture +def episode_sound(episode, sound): + obj = models.EpisodeSound(episode=episode, sound=sound, position=0, broadcast=sound.broadcast) + obj.save() + return obj + + @pytest.fixture def sounds(program): items = [ models.Sound( name=f"sound {i}", program=program, - type=models.Sound.TYPE_ARCHIVE, - position=i, + broadcast=True, file=f"sound-{i}.mp3", ) for i in range(0, 3) diff --git a/aircox_streamer/tests/test_controllers_monitor.py b/aircox_streamer/tests/test_controllers_monitor.py index f479765..4cef699 100644 --- a/aircox_streamer/tests/test_controllers_monitor.py +++ b/aircox_streamer/tests/test_controllers_monitor.py @@ -20,7 +20,7 @@ def monitor(streamer): @pytest.fixture -def diffusion(program, episode, sound): +def diffusion(program, episode, episode_sound): return baker.make( models.Diffusion, program=program, @@ -33,10 +33,10 @@ def diffusion(program, episode, sound): @pytest.fixture -def source(monitor, streamer, sound, diffusion): +def source(monitor, streamer, episode_sound, diffusion): source = next(monitor.streamer.playlists) - source.uri = sound.file.path - source.episode_id = sound.episode_id + source.uri = episode_sound.sound.file.path + source.episode_id = episode_sound.episode_id source.air_time = diffusion.start + tz.timedelta(seconds=10) return source @@ -185,7 +185,7 @@ class TestMonitor: monitor.trace_tracks(log) @pytest.mark.django_db(transaction=True) - def test_handle_diffusions(self, monitor, streamer, diffusion, sound): + def test_handle_diffusions(self, monitor, streamer, diffusion, episode_sound): interface( monitor, { diff --git a/aircox_streamer/tests/test_controllers_sources.py b/aircox_streamer/tests/test_controllers_sources.py index f620c47..be27446 100644 --- a/aircox_streamer/tests/test_controllers_sources.py +++ b/aircox_streamer/tests/test_controllers_sources.py @@ -67,7 +67,7 @@ class TestPlaylistSource: @pytest.mark.django_db def test_get_sound_queryset(self, playlist_source, sounds): query = playlist_source.get_sound_queryset() - assert all(r.program_id == playlist_source.program.pk and r.type == r.TYPE_ARCHIVE for r in query) + assert all(r.program_id == playlist_source.program.pk and r.broadcast for r in query) @pytest.mark.django_db def test_get_playlist(self, playlist_source, sounds):