@ -5,12 +5,69 @@ from django.db import migrations, models
 | 
				
			|||||||
import django.db.models.deletion
 | 
					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):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("aircox", "0025_sound_is_removed_alter_sound_is_downloadable_and_more"),
 | 
					        ("aircox", "0025_sound_is_removed_alter_sound_is_downloadable_and_more"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RunPython(get_sounds_info),
 | 
				
			||||||
        migrations.AlterModelOptions(
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
            name="sound",
 | 
					            name="sound",
 | 
				
			||||||
            options={"verbose_name": "Sound file", "verbose_name_plural": "Sound files"},
 | 
					            options={"verbose_name": "Sound file", "verbose_name_plural": "Sound files"},
 | 
				
			||||||
@ -105,4 +162,5 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                "verbose_name_plural": "Episode Sounds",
 | 
					                "verbose_name_plural": "Episode Sounds",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(restore_sounds_info),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,8 @@
 | 
				
			|||||||
import os
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.functional import cached_property
 | 
					from django.utils.functional import cached_property
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from filer.fields.image import FilerImageField
 | 
					from filer.fields.image import FilerImageField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox.conf import settings
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ("Station", "StationQuerySet", "Port")
 | 
					__all__ = ("Station", "StationQuerySet", "Port")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -32,13 +29,6 @@ class Station(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(_("name"), max_length=64)
 | 
					    name = models.CharField(_("name"), max_length=64)
 | 
				
			||||||
    slug = models.SlugField(_("slug"), max_length=64, unique=True)
 | 
					    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 = models.BooleanField(
 | 
				
			||||||
        _("default station"),
 | 
					        _("default station"),
 | 
				
			||||||
        default=False,
 | 
					        default=False,
 | 
				
			||||||
@ -96,12 +86,6 @@ class Station(models.Model):
 | 
				
			|||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, make_sources=True, *args, **kwargs):
 | 
					    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:
 | 
					        if self.default:
 | 
				
			||||||
            qs = Station.objects.filter(default=True)
 | 
					            qs = Station.objects.filter(default=True)
 | 
				
			||||||
            if self.pk is not None:
 | 
					            if self.pk is not None:
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,7 @@ class AdminMixin(LoginRequiredMixin, UserPassesTestMixin):
 | 
				
			|||||||
        return self.request.station
 | 
					        return self.request.station
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_func(self):
 | 
					    def test_func(self):
 | 
				
			||||||
        return self.request.user.is_staff
 | 
					        return self.request.user.is_admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs.update(admin.site.each_context(self.request))
 | 
					        kwargs.update(admin.site.each_context(self.request))
 | 
				
			||||||
 | 
				
			|||||||
@ -77,9 +77,12 @@ class Metadata:
 | 
				
			|||||||
            air_time = tz.datetime.strptime(air_time, "%Y/%m/%d %H:%M:%S")
 | 
					            air_time = tz.datetime.strptime(air_time, "%Y/%m/%d %H:%M:%S")
 | 
				
			||||||
            return local_tz.localize(air_time)
 | 
					            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
 | 
					        """Validate provided data and set as attribute (must already be
 | 
				
			||||||
        declared)"""
 | 
					        declared)"""
 | 
				
			||||||
 | 
					        if as_dict and isinstance(data, list):
 | 
				
			||||||
 | 
					            data = {v[0]: v[1] for v in data}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for key, value in data.items():
 | 
					        for key, value in data.items():
 | 
				
			||||||
            if hasattr(self, key) and not callable(getattr(self, key)):
 | 
					            if hasattr(self, key) and not callable(getattr(self, key)):
 | 
				
			||||||
                setattr(self, key, value)
 | 
					                setattr(self, key, value)
 | 
				
			||||||
 | 
				
			|||||||
@ -43,9 +43,9 @@ class Source(Metadata):
 | 
				
			|||||||
        except ValueError:
 | 
					        except ValueError:
 | 
				
			||||||
            self.remaining = None
 | 
					            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:
 | 
					        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):
 | 
					    def skip(self):
 | 
				
			||||||
        """Skip the current source sound."""
 | 
					        """Skip the current source sound."""
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,7 @@ import subprocess
 | 
				
			|||||||
import psutil
 | 
					import psutil
 | 
				
			||||||
from django.template.loader import render_to_string
 | 
					from django.template.loader import render_to_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox.conf import settings
 | 
					from ..conf import settings
 | 
				
			||||||
 | 
					 | 
				
			||||||
from ..connector import Connector
 | 
					from ..connector import Connector
 | 
				
			||||||
from .sources import PlaylistSource, QueueSource
 | 
					from .sources import PlaylistSource, QueueSource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -46,8 +45,8 @@ class Streamer:
 | 
				
			|||||||
        self.outputs = self.station.port_set.active().output()
 | 
					        self.outputs = self.station.port_set.active().output()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.id = self.station.slug.replace("-", "_")
 | 
					        self.id = self.station.slug.replace("-", "_")
 | 
				
			||||||
        self.path = os.path.join(station.path, "station.liq")
 | 
					        self.path = settings.get_dir(station, "station.liq")
 | 
				
			||||||
        self.connector = connector or Connector(os.path.join(station.path, "station.sock"))
 | 
					        self.connector = connector or Connector(settings.get_dir(station, "station.sock"))
 | 
				
			||||||
        self.init_sources()
 | 
					        self.init_sources()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -98,7 +97,6 @@ class Streamer:
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "station": self.station,
 | 
					                "station": self.station,
 | 
				
			||||||
                "streamer": self,
 | 
					                "streamer": self,
 | 
				
			||||||
                "settings": settings,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        data = re.sub("[\t ]+\n", "\n", data)
 | 
					        data = re.sub("[\t ]+\n", "\n", data)
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,7 @@ from aircox_streamer.controllers import Monitor, Streamer
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# force using UTC
 | 
					# force using UTC
 | 
				
			||||||
tz.activate(timezone.UTC)
 | 
					tz.activate(timezone.utc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Command(BaseCommand):
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
				
			|||||||
@ -10,9 +10,9 @@ Base liquidsoap station configuration.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block functions %}
 | 
					{% block functions %}
 | 
				
			||||||
{# Seek function #}
 | 
					{# Seek function #}
 | 
				
			||||||
def seek(source, t) =
 | 
					def seek(s, t) =
 | 
				
			||||||
  t = float_of_string(default=0.,t)
 | 
					  t = float_of_string(default=0.,t)
 | 
				
			||||||
  ret = source.seek(source,t)
 | 
					  ret = source.seek(s,t)
 | 
				
			||||||
  log("seek #{ret} seconds.")
 | 
					  log("seek #{ret} seconds.")
 | 
				
			||||||
  "#{ret}"
 | 
					  "#{ret}"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
@ -30,6 +30,17 @@ def to_stream(live, stream)
 | 
				
			|||||||
  add(normalize=false, [live,stream])
 | 
					  add(normalize=false, [live,stream])
 | 
				
			||||||
end
 | 
					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 %}
 | 
					{% comment %}
 | 
				
			||||||
An interactive source is a source that:
 | 
					An interactive source is a source that:
 | 
				
			||||||
@ -45,10 +56,14 @@ def interactive (id, s) =
 | 
				
			|||||||
    server.register(namespace=id,
 | 
					    server.register(namespace=id,
 | 
				
			||||||
                    description="Get source's track remaining time",
 | 
					                    description="Get source's track remaining time",
 | 
				
			||||||
                    usage="remaining",
 | 
					                    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)
 | 
					    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
 | 
					    s
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -66,9 +81,6 @@ end
 | 
				
			|||||||
set("server.socket", true)
 | 
					set("server.socket", true)
 | 
				
			||||||
set("server.socket.path", "{{ streamer.socket_path }}")
 | 
					set("server.socket.path", "{{ streamer.socket_path }}")
 | 
				
			||||||
set("log.file.path", "{{ station.path }}/liquidsoap.log")
 | 
					set("log.file.path", "{{ station.path }}/liquidsoap.log")
 | 
				
			||||||
{% for key, value in settings.AIRCOX_LIQUIDSOAP_SET.items %}
 | 
					 | 
				
			||||||
set("{{ key|safe }}", {{ value|safe }})
 | 
					 | 
				
			||||||
{% endfor %}
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block config_extras %}
 | 
					{% block config_extras %}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,13 +20,18 @@ from django.contrib import admin
 | 
				
			|||||||
from django.urls import include, path
 | 
					from django.urls import include, path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import aircox.urls
 | 
					import aircox.urls
 | 
				
			||||||
 | 
					import aircox_streamer.urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = aircox.urls.urls + [
 | 
					urlpatterns = (
 | 
				
			||||||
    path("admin/", admin.site.urls),
 | 
					    aircox.urls.urls
 | 
				
			||||||
    path("accounts/", include("django.contrib.auth.urls")),
 | 
					    + aircox_streamer.urls.urls
 | 
				
			||||||
    path("ckeditor/", include("ckeditor_uploader.urls")),
 | 
					    + [
 | 
				
			||||||
    path("filer/", include("filer.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:
 | 
					if settings.DEBUG:
 | 
				
			||||||
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(
 | 
					    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user