diff --git a/aircox/migrations/0026_alter_sound_options_remove_sound_episode_and_more.py b/aircox/migrations/0026_alter_sound_options_remove_sound_episode_and_more.py index b4dacf3..fb9a131 100644 --- a/aircox/migrations/0026_alter_sound_options_remove_sound_episode_and_more.py +++ b/aircox/migrations/0026_alter_sound_options_remove_sound_episode_and_more.py @@ -5,12 +5,69 @@ from django.db import migrations, models import django.db.models.deletion +sounds_info = {} + + +def get_sounds_info(apps, schema_editor): + global sounds + + Sound = apps.get_model("aircox", "Sound") + objs = Sound.objects.filter(episode__isnull=False).values( + "pk", + "episode_id", + "position", + "type", + ) + sounds_info.update({obj["pk"]: obj for obj in objs}) + + +def restore_sounds_info(apps, schema_editor): + global sounds + + try: + Sound = apps.get_model("aircox", "Sound") + EpisodeSound = apps.get_model("aircox", "EpisodeSound") + TYPE_ARCHIVE = 0x01 + TYPE_REMOVED = 0x03 + + episode_sounds = [] + sounds = [] + for sound in Sound.objects.all(): + info = sounds_info.get(sound.pk) + if not info: + continue + + sound.broadcast = info["type"] == TYPE_ARCHIVE + sound.is_removed = info["type"] == TYPE_REMOVED + sounds.append(sound) + if not sound.is_removed: + obj = EpisodeSound( + sound=sound, + episode_id=info["episode_id"], + position=info["position"], + broadcast=sound.broadcast, + ) + episode_sounds.append(obj) + + Sound.objects.bulk_update(sounds, ("broadcast", "is_removed")) + EpisodeSound.objects.bulk_create(episode_sounds) + + print(f"\n>>> {len(sounds)} Sound have been updated.") + print(f">>> {len(episode_sounds)} EpisodeSound have been created.") + except Exception as err: + print(err) + import traceback + + traceback.print_exc() + + class Migration(migrations.Migration): dependencies = [ ("aircox", "0025_sound_is_removed_alter_sound_is_downloadable_and_more"), ] operations = [ + migrations.RunPython(get_sounds_info), migrations.AlterModelOptions( name="sound", options={"verbose_name": "Sound file", "verbose_name_plural": "Sound files"}, @@ -105,4 +162,5 @@ class Migration(migrations.Migration): "verbose_name_plural": "Episode Sounds", }, ), + migrations.RunPython(restore_sounds_info), ] diff --git a/aircox/models/station.py b/aircox/models/station.py index 8c109b2..65c1090 100644 --- a/aircox/models/station.py +++ b/aircox/models/station.py @@ -1,11 +1,8 @@ -import os - from django.db import models from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from filer.fields.image import FilerImageField -from aircox.conf import settings __all__ = ("Station", "StationQuerySet", "Port") @@ -32,13 +29,6 @@ class Station(models.Model): name = models.CharField(_("name"), max_length=64) slug = models.SlugField(_("slug"), max_length=64, unique=True) - # FIXME: remove - should be decided only by Streamer controller + settings - path = models.CharField( - _("path"), - help_text=_("path to the working directory"), - max_length=256, - blank=True, - ) default = models.BooleanField( _("default station"), default=False, @@ -96,12 +86,6 @@ class Station(models.Model): return self.name def save(self, make_sources=True, *args, **kwargs): - if not self.path: - self.path = os.path.join( - settings.CONTROLLERS_WORKING_DIR, - self.slug.replace("-", "_"), - ) - if self.default: qs = Station.objects.filter(default=True) if self.pk is not None: diff --git a/aircox/views/admin.py b/aircox/views/admin.py index a76ddae..d66a6fd 100644 --- a/aircox/views/admin.py +++ b/aircox/views/admin.py @@ -19,7 +19,7 @@ class AdminMixin(LoginRequiredMixin, UserPassesTestMixin): return self.request.station def test_func(self): - return self.request.user.is_staff + return self.request.user.is_admin def get_context_data(self, **kwargs): kwargs.update(admin.site.each_context(self.request)) diff --git a/aircox_streamer/controllers/metadata.py b/aircox_streamer/controllers/metadata.py index 6c3ddab..112db44 100755 --- a/aircox_streamer/controllers/metadata.py +++ b/aircox_streamer/controllers/metadata.py @@ -77,9 +77,12 @@ class Metadata: air_time = tz.datetime.strptime(air_time, "%Y/%m/%d %H:%M:%S") return local_tz.localize(air_time) - def validate(self, data): + def validate(self, data, as_dict=False): """Validate provided data and set as attribute (must already be declared)""" + if as_dict and isinstance(data, list): + data = {v[0]: v[1] for v in data} + for key, value in data.items(): if hasattr(self, key) and not callable(getattr(self, key)): setattr(self, key, value) diff --git a/aircox_streamer/controllers/sources.py b/aircox_streamer/controllers/sources.py index 0981bae..d994d03 100755 --- a/aircox_streamer/controllers/sources.py +++ b/aircox_streamer/controllers/sources.py @@ -43,9 +43,9 @@ class Source(Metadata): except ValueError: self.remaining = None - data = self.controller.send(self.id, ".get", parse=True) + data = self.controller.send(f"var.get {self.id}_meta", parse_json=True) if data: - self.validate(data if data and isinstance(data, dict) else {}) + self.validate(data if data and isinstance(data, (dict, list)) else {}, as_dict=True) def skip(self): """Skip the current source sound.""" diff --git a/aircox_streamer/controllers/streamer.py b/aircox_streamer/controllers/streamer.py index 5d19101..3e3e666 100755 --- a/aircox_streamer/controllers/streamer.py +++ b/aircox_streamer/controllers/streamer.py @@ -8,8 +8,7 @@ import subprocess import psutil from django.template.loader import render_to_string -from aircox.conf import settings - +from ..conf import settings from ..connector import Connector from .sources import PlaylistSource, QueueSource @@ -46,8 +45,8 @@ class Streamer: self.outputs = self.station.port_set.active().output() self.id = self.station.slug.replace("-", "_") - self.path = os.path.join(station.path, "station.liq") - self.connector = connector or Connector(os.path.join(station.path, "station.sock")) + self.path = settings.get_dir(station, "station.liq") + self.connector = connector or Connector(settings.get_dir(station, "station.sock")) self.init_sources() @property @@ -98,7 +97,6 @@ class Streamer: { "station": self.station, "streamer": self, - "settings": settings, }, ) data = re.sub("[\t ]+\n", "\n", data) diff --git a/aircox_streamer/management/commands/streamer.py b/aircox_streamer/management/commands/streamer.py index 975ff10..13c59e2 100755 --- a/aircox_streamer/management/commands/streamer.py +++ b/aircox_streamer/management/commands/streamer.py @@ -19,7 +19,7 @@ from aircox_streamer.controllers import Monitor, Streamer # force using UTC -tz.activate(timezone.UTC) +tz.activate(timezone.utc) class Command(BaseCommand): diff --git a/aircox_streamer/templates/aircox_streamer/scripts/station.liq b/aircox_streamer/templates/aircox_streamer/scripts/station.liq index 26ac7dd..1b5ccd7 100755 --- a/aircox_streamer/templates/aircox_streamer/scripts/station.liq +++ b/aircox_streamer/templates/aircox_streamer/scripts/station.liq @@ -10,9 +10,9 @@ Base liquidsoap station configuration. {% block functions %} {# Seek function #} -def seek(source, t) = +def seek(s, t) = t = float_of_string(default=0.,t) - ret = source.seek(source,t) + ret = source.seek(s,t) log("seek #{ret} seconds.") "#{ret}" end @@ -30,6 +30,17 @@ def to_stream(live, stream) add(normalize=false, [live,stream]) end +{# Skip command #} +def add_skip_command(s) = + def skip(_) = + source.skip(s) + "Done!" + end + server.register(namespace="#{source.id(s)}", + usage="skip", + description="Skip the current song.", + "skip",skip) +end {% comment %} An interactive source is a source that: @@ -45,10 +56,14 @@ def interactive (id, s) = server.register(namespace=id, description="Get source's track remaining time", usage="remaining", - "remaining", fun (_) -> begin json_of(source.remaining(s)) end) + "remaining", fun (_) -> begin json.stringify(source.remaining(s)) end) - s = store_metadata(id=id, size=1, s) add_skip_command(s) + + {# metadata: create an interactive variable as "{id}_meta" #} + s_meta = interactive.string("#{id}_meta", "") + s = source.on_metadata(s, fun(meta) -> s_meta.set(json.stringify(meta))) + s end @@ -66,9 +81,6 @@ end set("server.socket", true) set("server.socket.path", "{{ streamer.socket_path }}") set("log.file.path", "{{ station.path }}/liquidsoap.log") -{% for key, value in settings.AIRCOX_LIQUIDSOAP_SET.items %} -set("{{ key|safe }}", {{ value|safe }}) -{% endfor %} {% endblock %} {% block config_extras %} diff --git a/instance/urls.py b/instance/urls.py index 1acda1b..06b2733 100755 --- a/instance/urls.py +++ b/instance/urls.py @@ -20,13 +20,18 @@ from django.contrib import admin from django.urls import include, path import aircox.urls +import aircox_streamer.urls -urlpatterns = aircox.urls.urls + [ - path("admin/", admin.site.urls), - path("accounts/", include("django.contrib.auth.urls")), - path("ckeditor/", include("ckeditor_uploader.urls")), - path("filer/", include("filer.urls")), -] +urlpatterns = ( + aircox.urls.urls + + aircox_streamer.urls.urls + + [ + path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), + path("ckeditor/", include("ckeditor_uploader.urls")), + path("filer/", include("filer.urls")), + ] +) if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(