fixed music load, added radio

This commit is contained in:
Alexander Karpov 2023-07-10 03:13:42 +03:00
parent 72f17c8ee2
commit bb61d144c5
23 changed files with 952 additions and 104 deletions

View File

@ -1,4 +1,5 @@
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
from akarpov.common.jwt import read_jwt
@ -29,3 +30,13 @@ async def __call__(self, scope, receive, send):
scope["user"] = await get_user(dict(scope["headers"]))
return await self.app(scope, receive, send)
class BaseConsumer(AsyncJsonWebsocketConsumer):
async def send_error(self, msg):
await self.send_json({"type": "error", "data": {"msg": msg}})
class SyncBaseConsumer(JsonWebsocketConsumer):
def send_error(self, msg):
self.send_json({"type": "error", "data": {"msg": msg}})

View File

@ -0,0 +1,44 @@
from rest_framework import serializers
from akarpov.music.models import Album, Author, Song
class AuthorSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField(method_name="get_url")
def get_url(self, obj):
return obj.get_absolute_url()
class Meta:
model = Author
fields = ["name", "link", "image_cropped", "url"]
class AlbumSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField(method_name="get_url")
def get_url(self, obj):
return obj.get_absolute_url()
class Meta:
model = Album
fields = ["name", "link", "image_cropped", "url"]
class SongSerializer(serializers.ModelSerializer):
authors = AuthorSerializer(many=True)
album = AlbumSerializer()
class Meta:
model = Song
fields = [
"id",
"image",
"link",
"length",
"played",
"name",
"file",
"authors",
"album",
]

View File

@ -1,4 +1,5 @@
from django.apps import AppConfig
from django.core.exceptions import AppRegistryNotReady
class MusicConfig(AppConfig):
@ -10,3 +11,12 @@ def ready(self):
import akarpov.music.signals # noqa F401
except ImportError:
pass
try:
from akarpov.music.tasks import start_next_song
start_next_song.apply_async(
kwargs={"previous_ids": []},
countdown=5,
)
except AppRegistryNotReady:
pass

View File

@ -0,0 +1,51 @@
import json
from channels.db import database_sync_to_async
from django.utils.timezone import now
from akarpov.common.channels import BaseConsumer
from akarpov.music.api.serializers import SongSerializer
from akarpov.music.models import RadioSong
class RadioConsumer(BaseConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.room_group_name = None
async def connect(self):
self.room_group_name = "radio_main"
await self.accept()
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
data = await self.get_radio_song()
if data:
await self.send_json(data)
@database_sync_to_async
def get_radio_song(self):
r = RadioSong.objects.filter(slug="")
if r:
r = r.first()
return SongSerializer(context={"request": None}).to_representation(
r.song
) | {"offset": (now() - r.start).seconds}
return None
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
async def receive(self, text_data):
data = None
try:
data = json.loads(text_data)
except ValueError:
await self.send_json(
{"type": "ERROR", "message": "data is not JSON serializable"}
)
return data
async def song(self, event):
data = event["data"]
await self.send_json(data)

View File

@ -0,0 +1,37 @@
# Generated by Django 4.2.3 on 2023-07-09 19:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("music", "0003_remove_song_author_remove_songinque_song_and_more"),
]
operations = [
migrations.CreateModel(
name="RadioSong",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("slug", models.SlugField(unique=True)),
(
"song",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="radio",
to="music.song",
),
),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2023-07-09 19:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("music", "0004_radiosong"),
]
operations = [
migrations.AddField(
model_name="radiosong",
name="start",
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -83,3 +83,9 @@ class SongInQue(models.Model):
name = models.CharField(blank=True, max_length=250)
status = models.CharField(null=True, blank=True, max_length=250)
error = models.BooleanField(default=False)
class RadioSong(models.Model):
start = models.DateTimeField(auto_now=True)
slug = models.SlugField(unique=True)
song = models.ForeignKey("Song", related_name="radio", on_delete=models.CASCADE)

View File

@ -1,6 +1,8 @@
import os
from django.core.files import File
from mutagen import File as MutagenFile
from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
from mutagen.mp3 import MP3
from PIL import Image
from pydub import AudioSegment
@ -15,10 +17,11 @@ def load_track(
album: str | None = None,
name: str | None = None,
link: str | None = None,
**kwargs,
) -> Song:
p_name = path.split("/")[-1]
if authors:
authors = [Author.objects.get_or_create(name=x)[0] for x in authors]
authors = [Author.objects.get_or_create(name=x)[0] for x in authors if authors]
else:
authors = []
if album:
@ -27,7 +30,9 @@ def load_track(
album = None
if sng := Song.objects.filter(
name=name if name else p_name, authors=authors, album=album
name=name if name else p_name,
authors__id__in=[x.id for x in authors],
album=album,
):
return sng.first()
@ -37,8 +42,7 @@ def load_track(
os.remove(path)
path = mp3_path
audio = MP3(path)
tag = MP3(path, ID3=ID3)
if image_path:
if not image_path.endswith(".png"):
im = Image.open(image_path)
@ -46,18 +50,50 @@ def load_track(
im.save(image_path)
song = Song(
link=link, length=audio.info.length, name=name if name else p_name, album=album
link=link if link else "",
length=tag.info.length,
name=name if name else p_name,
album=album,
)
with open(path, "rb") as file:
song.file = File(file, name=path.split("/")[-1])
if image_path:
with open(image_path, "rb") as file:
song.image = File(file, name=image_path.split("/")[-1])
with open(path, "rb") as file, open(image_path, "rb") as image:
song.image = File(image, name=image_path.split("/")[-1])
song.file = File(file, name=path.split("/")[-1])
song.save()
else:
with open(path, "rb") as file:
song.file = File(file, name=path.split("/")[-1])
song.save()
if authors:
song.authors.set(authors)
song.save()
# set music meta
tag = MutagenFile(song.file.path)
tag["title"] = TextFrame(encoding=3, text=[name])
if album:
tag["album"] = TextFrame(encoding=3, text=[album.name])
if authors:
tag["artist"] = TextFrame(encoding=3, text=[x.name for x in authors])
tag.save()
tag = MP3(song.file.path, ID3=ID3)
if image_path:
with open(image_path, "rb") as f:
tag.tags.add(
APIC(
encoding=3, # 3 is for utf-8
mime="image/png", # image/jpeg or image/png
type=3, # 3 is for the cover image
desc="Cover",
data=f.read(),
)
)
if "release" in kwargs:
tag.tags.add(TORY(text=kwargs["release"]))
if "genre" in kwargs:
tag.tags.add(TCON(text=kwargs["genre"]))
tag.save()
return song

View File

@ -1,49 +1,60 @@
import os
from io import BytesIO
from pathlib import Path
from random import randint
import mutagen
from django.core.files import File
from mutagen.id3 import ID3
from PIL import Image, UnidentifiedImageError
from akarpov.music.models import Album, Author, Song, SongInQue
from akarpov.music.models import Song
from akarpov.music.services.db import load_track
def load_dir(path: str):
path = Path(path)
for f in list(path.glob("**/*.mp3")):
with f.open("rb") as file:
process_mp3_file(File(file, name=str(f).split("/")[-1]), str(f))
process_mp3_file(str(f))
def load_file(path: str):
with open(path, "rb") as file:
process_mp3_file(File(file, name=path.split("/")[-1]), path)
process_mp3_file(path)
def process_mp3_file(file: File, path: str) -> None:
que = SongInQue.objects.create()
try:
tag = mutagen.File(path, easy=True)
que.name = tag["title"][0] if "title" in tag else path.split("/")[-1]
que.save()
if "artist" in tag:
author = Author.objects.get_or_create(name=tag["artist"][0])[0]
else:
author = None
def process_mp3_file(path: str) -> None:
tag = mutagen.File(path, easy=True)
if "artist" in tag:
author = tag["artist"]
else:
author = None
if "album" in tag:
album = Album.objects.get_or_create(name=tag["album"][0])[0]
else:
album = None
if "album" in tag:
album = tag["album"]
else:
album = None
name = tag["title"][0] if "title" in tag else path.split("/")[-1]
f = Song.objects.filter(name=name)
if author:
f.filter(authors__name__in=author)
if album:
f.filter(album__name=album)
if f.exists():
return
song, created = Song.objects.get_or_create(
name=tag["title"][0] if "title" in tag else path.split("/")[-1],
author=author,
album=album,
)
song.file = file
song.save(update_fields=["file"])
que.delete()
except Exception as e:
que.name = e
que.error = True
que.save()
tags = ID3(path)
pict = [x for x in tags.getall("APIC") if x]
image_pth = None
if pict:
try:
pict = pict[0].data
im = Image.open(BytesIO(pict))
image_pth = f"/tmp/{randint(1, 1000000)}.png"
if os.path.exists(image_pth):
image_pth = f"/tmp/{randint(1, 1000000)}.png"
im.save(image_pth)
except UnidentifiedImageError:
pass
load_track(path, image_pth, author, album, name)
if image_pth and os.path.exists(image_pth):
os.remove(image_pth)

View File

@ -1,18 +1,14 @@
import os
from pathlib import Path
from random import randint
import requests
from django.conf import settings
from django.core.files import File
from mutagen.easyid3 import EasyID3
from mutagen.id3 import APIC, ID3, TCON, TORY
from mutagen.mp3 import MP3
from PIL import Image
from pydub import AudioSegment
from pytube import Search, YouTube
from akarpov.music.models import Album, Author, Song, SongInQue
from akarpov.music.models import Song, SongInQue
from akarpov.music.services.db import load_track
from akarpov.music.services.spotify import get_track_info
@ -37,9 +33,6 @@ def download_from_youtube_link(link: str) -> Song:
que.delete()
return sng.first()
authors = [Author.objects.get_or_create(name=x)[0] for x in info["artists"]]
album = Album.objects.get_or_create(name=info["album_name"])[0]
audio = yt.streams.filter(only_audio=True).order_by("abr").desc().first()
orig_path = audio.download(output_path=settings.MEDIA_ROOT)
@ -52,7 +45,7 @@ def download_from_youtube_link(link: str) -> Song:
r = requests.get(info["album_image"])
img_pth = str(
settings.MEDIA_ROOT
+ f"/{info['album_image'].split('/')[-1]}_{str(randint(100, 999))}"
+ f"/{info['album_image'].split('/')[-1]}_{str(randint(100, 999))}.png"
)
with open(img_pth, "wb") as f:
f.write(r.content)
@ -62,41 +55,9 @@ def download_from_youtube_link(link: str) -> Song:
os.remove(img_pth)
# set music meta
tag = MP3(path, ID3=ID3)
tag.tags.add(
APIC(
encoding=3, # 3 is for utf-8
mime="image/png", # image/jpeg or image/png
type=3, # 3 is for the cover image
desc="Cover",
data=open(str(f"{img_pth}.png"), "rb").read(),
)
)
tag.tags.add(TORY(text=info["release"]))
if "genre" in info:
tag.tags.add(TCON(text=info["genre"]))
tag.save()
os.remove(str(f"{img_pth}.png"))
tag = EasyID3(path)
tag["title"] = info["title"]
tag["album"] = info["album_name"]
tag["artist"] = info["artist"]
tag.save()
# save track
ms_path = Path(path)
song = Song(name=info["title"], author=authors[0], album=album)
with ms_path.open(mode="rb") as f:
song.file = File(f, name=ms_path.name)
song.save()
os.remove(path)
que.delete()
return song
load_track(path, img_pth, info["artists"], info["album_name"])
except Exception as e:
print(e)
que.name = e
que.error = True
que.save()

View File

@ -1,8 +1,13 @@
from asgiref.sync import async_to_sync
from celery import shared_task
from channels.layers import get_channel_layer
from pytube import Channel, Playlist
from akarpov.music.api.serializers import SongSerializer
from akarpov.music.models import RadioSong, Song
from akarpov.music.services import yandex, youtube
from akarpov.music.services.file import load_dir, load_file
from akarpov.utils.celery import get_scheduled_tasks_name
@shared_task
@ -44,3 +49,39 @@ def process_file(path):
@shared_task
def load_ym_file_meta(track):
return yandex.load_file_meta(track)
@shared_task()
def start_next_song(previous_ids: list):
f = Song.objects.filter(length__isnull=False).exclude(id__in=previous_ids)
if not f:
previous_ids = []
f = Song.objects.filter(length__isnull=False)
if not f:
if "akarpov.music.tasks.start_next_song" not in get_scheduled_tasks_name():
start_next_song.apply_async(
kwargs={"previous_ids": []},
countdown=60,
)
else:
song = f.order_by("?").first()
data = SongSerializer(context={"request": None}).to_representation(song)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"radio_main", {"type": "song", "data": data}
)
song.played += 1
song.save(update_fields=["played"])
if RadioSong.objects.filter(slug="").exists():
r = RadioSong.objects.get(slug="")
r.song = song
r.save()
else:
RadioSong.objects.create(song=song, slug="")
previous_ids.append(song.id)
if "akarpov.music.tasks.start_next_song" not in get_scheduled_tasks_name():
start_next_song.apply_async(
kwargs={"previous_ids": previous_ids},
countdown=song.length,
)
return

View File

@ -11,4 +11,5 @@
path("album/<str:slug>", views.album_view, name="album"),
path("author/<str:slug>", views.author_view, name="author"),
path("playlist/<str:slug>", views.playlist_view, name="playlist"),
path("radio/", views.radio_main_view, name="radio"),
]

View File

@ -48,13 +48,14 @@ class PlaylistView(generic.DetailView):
class LoadTrackView(SuperUserRequiredMixin, generic.FormView):
form_class = TracksLoadForm
template_name = "music/upload.html"
def get_success_url(self):
# TODO: add room to see tracks load
return ""
def form_valid(self, form):
load_tracks(form.address)
load_tracks(form.data["address"])
return super().form_valid(form)
@ -76,3 +77,10 @@ def form_valid(self, form):
load_track_file_view = LoadTrackFileView.as_view()
class MainRadioView(generic.TemplateView):
template_name = "music/radio.html"
radio_main_view = MainRadioView.as_view()

View File

@ -0,0 +1,330 @@
{% extends 'base.html' %}
{% block css %}
<style>
body {
background-color: #EDEDED;
}
h1 {
font-family: "Open Sans", sans-serif;
font-size: 13pt;
font-weight: 600;
text-transform: uppercase;
color: white;
cursor: default;
}
h4 {
font-family: "Open Sans", sans-serif;
font-size: 8pt;
font-weight: 400;
cursor: default;
}
h2 {
font-family: "Open Sans", sans-serif;
font-size: 13pt;
font-weight: 300;
color: white;
cursor: default;
}
.player {
height: 190px;
width: 430px;
background-color: #1E2125;
position: absolute;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
}
.player ul {
list-style: none;
}
.player ul li {
display: inline-block;
}
.cover {
position: absolute;
top: 0;
left: 0;
}
.cover img {
height: 190px;
width: 190px;
}
.info h1 {
margin-top: 15px;
margin-left: 180px;
line-height: 0;
}
.info h4 {
margin-left: 180px;
line-height: 20px;
color: #636367;
}
.info h2 {
margin-left: 180px;
}
.button-items {
margin-left: 180px;
}
#slider {
width: 182px;
height: 4px;
background: #151518;
border-radius: 2px;
}
#slider div {
width: 4px;
height: 4px;
margin-top: 1px;
background: #EF6DBC;
border-radius: 2px;
}
#timer {
color: #494B4E;
line-height: 0;
font-size: 9pt;
float: right;
font-family: Arial, Sans-Serif;
}
.controls {
margin-top: 20px;
}
.controls svg:nth-child(2) {
margin-left: 5px;
margin-right: 5px;
}
#play {
padding: 0 3px;
width: 30px;
height: 30px;
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
}
#play g {
stroke: #FEFEFE;
stroke-width: 1;
stroke-miterlimit: 10;
}
#play g path {
fill: #FEFEFE;
}
#play:hover {
cursor: pointer;
}
#play:hover g {
stroke: #8F4DA9;
cursor: pointer;
}
#play:hover g path {
fill: #9b59b6;
cursor: pointer;
}
.step-backward {
width: 18px;
height: 18px;
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
margin-bottom: 5px;
}
.step-backward g polygon {
fill: #FEFEFE;
}
.step-foreward {
width: 18px;
height: 18px;
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
margin-bottom: 5px;
}
.step-foreward g polygon {
fill: #FEFEFE;
}
#pause {
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
width: 30px;
height: 30px;
position: absolute;
margin-left: -38px;
cursor: pointer;
}
#pause rect {
fill: white;
}
#pause:hover rect {
fill: #8F4DA9;
}
.step-backward g polygon:hover, .step-foreward g polygon:hover {
fill: #EF6DBC;
cursor: pointer;
}
#skip p {
color: #2980b9;
}
#skip p:hover {
color: #e74c3c;
cursor: pointer;
}
.expend {
padding: 0.5px;
cursor: pointer;
}
.expend svg:hover g polygon {
fill: #EF6DBC;
}
</style>
{% endblock %}
{% block content %}
<div class="player">
<ul>
<li class="cover"><img id="cover" src="" alt=""/></li>
<li class="info">
<h1 id="artist"></h1>
<h4 id="album"></h4>
<h2 id="name">I Need You Back</h2>
<div class="button-items">
<audio id="music">
<source id="music-src" type="audio/mp3">
</audio>
<div id="slider"><div id="elapsed"></div></div>
<p id="timer">0:00</p>
<div class="controls">
<span class="expend"><svg class="step-backward" viewBox="0 0 25 25" xml:space="preserve">
<g><polygon points="4.9,4.3 9,4.3 9,11.6 21.4,4.3 21.4,20.7 9,13.4 9,20.7 4.9,20.7"/></g>
</svg></span>
<svg id="play" viewBox="0 0 25 25" xml:space="preserve">
<defs><rect x="-49.5" y="-132.9" width="446.4" height="366.4"/></defs>
<g><circle fill="none" cx="12.5" cy="12.5" r="10.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.7,6.9V18c0,0,0.2,1.4,1.8,0l8.1-4.8c0,0,1.2-1.1-1-2L9.8,6.5 C9.8,6.5,9.1,6,8.7,6.9z"/>
</g>
</svg>
<svg id="pause" viewBox="0 0 25 25" xml:space="preserve">
<g>
<rect x="6" y="4.6" width="3.8" height="15.7"/>
<rect x="14" y="4.6" width="3.9" height="15.7"/>
</g>
</svg>
<span class="expend"><svg class="step-foreward" viewBox="0 0 25 25" xml:space="preserve">
<g><polygon points="20.7,4.3 16.6,4.3 16.6,11.6 4.3,4.3 4.3,20.7 16.7,13.4 16.6,20.7 20.7,20.7"/></g>
</svg></span>
</div>
</div>
</li>
</ul>
</div>
{% endblock %}
{% block inline_javascript %}
<script>
const music = document.getElementById("music");
async function playAudio() {
try {
await music.play();
playButton.style.visibility = "hidden";
pause.style.visibility = "visible";
} catch (err) {
playButton.style.visibility = "visible";
pause.style.visibility = "hidden";
}
}
const music_src = document.getElementById("music-src")
const cover_src = document.getElementById("cover")
const name = document.getElementById("name")
const album = document.getElementById("album")
const artist = document.getElementById("artist")
const socket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/radio/'
);
socket.onmessage = function(e) {
const data = JSON.parse(e.data);
console.log(data)
music_src.src = data.file
if (data.image !== null) {
cover_src.src = data.image
}
name.innerText = data.name
playAudio()
};
socket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
const playButton = document.getElementById("play");
const pauseButton = document.getElementById("pause");
const playhead = document.getElementById("elapsed");
const timeline = document.getElementById("slider");
const timer = document.getElementById("timer");
let duration;
pauseButton.style.visibility = "hidden";
const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
music.addEventListener("timeupdate", timeUpdate, false);
function timeUpdate() {
const playPercent = timelineWidth * (music.currentTime / duration);
playhead.style.width = playPercent + "px";
const secondsIn = Math.floor(((music.currentTime / duration) / 3.5) * 100);
if (secondsIn <= 9) {
timer.innerHTML = "0:0" + secondsIn;
} else {
timer.innerHTML = "0:" + secondsIn;
}
}
playButton.onclick = function() {
playAudio()
}
pauseButton.onclick = function() {
music.pause();
playButton.style.visibility = "visible";
pause.style.visibility = "hidden";
}
music.addEventListener("canplaythrough", function () {
duration = music.duration;
}, false);
</script>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}editing post on akarpov{% endblock %}
{% block content %}
<form class="pt-2" enctype="multipart/form-data" method="POST" id="designer-form">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{{ field|as_crispy_field }}
{% endfor %}
<div class="mt-4 flex justify-end space-x-4">
<button class="btn btn-secondary" type="submit" id="submit">
Upload
</button>
</div>
</form>
{% endblock %}

10
akarpov/utils/celery.py Normal file
View File

@ -0,0 +1,10 @@
from config.celery_app import app
def get_scheduled_tasks_name() -> [str]:
i = app.control.inspect()
t = i.scheduled()
all_tasks = []
for worker, tasks in t.items():
all_tasks += tasks
return [x["request"]["name"] for x in all_tasks]

View File

@ -1,24 +1,12 @@
import functools
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
def login_required(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
if not self.scope.get("user", False) or not self.scope["user"].is_authenticated:
self.send_error("Требуется авторизация")
self.send_error("Auth is required")
else:
return func(self, *args, **kwargs)
return wrapper
class BaseConsumer(AsyncJsonWebsocketConsumer):
async def send_error(self, msg):
await self.send_json({"type": "error", "data": {"msg": msg}})
class SyncBaseConsumer(JsonWebsocketConsumer):
def send_error(self, msg):
self.send_json({"type": "error", "data": {"msg": msg}})

View File

@ -1,12 +1,16 @@
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf import settings
from django.core.asgi import get_asgi_application
from akarpov.common.channels import HeaderAuthMiddleware
from config import routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
if settings.DEBUG:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
else:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
application = ProtocolTypeRouter(
{

View File

@ -1 +1,7 @@
websocket_urlpatterns = []
from django.urls import re_path
from akarpov.music.consumers import RadioConsumer
websocket_urlpatterns = [
re_path(r"ws/radio/", RadioConsumer.as_asgi()),
]

View File

@ -76,6 +76,18 @@
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
# CHANNELS
# ------------------------------------------------------------------------------
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
# APPS
# ------------------------------------------------------------------------------
DJANGO_APPS = [

243
poetry.lock generated
View File

@ -180,6 +180,18 @@ imageio = ">=1.5"
numpy = ">=1.11.1"
Pillow = ">=3.3.1"
[[package]]
name = "annotated-types"
version = "0.5.0"
description = "Reusable constraint types to use with typing.Annotated"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"},
{file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"},
]
[[package]]
name = "anyio"
version = "3.7.1"
@ -726,6 +738,28 @@ Django = ">=3.2"
daphne = ["daphne (>=4.0.0)"]
tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"]
[[package]]
name = "channels-redis"
version = "4.1.0"
description = "Redis-backed ASGI channel layer implementation"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "channels_redis-4.1.0-py3-none-any.whl", hash = "sha256:3696f5b9fe367ea495d402ba83d7c3c99e8ca0e1354ff8d913535976ed0abf73"},
{file = "channels_redis-4.1.0.tar.gz", hash = "sha256:6bd4f75f4ab4a7db17cee495593ace886d7e914c66f8214a1f247ff6659c073a"},
]
[package.dependencies]
asgiref = ">=3.2.10,<4"
channels = "*"
msgpack = ">=1.0,<2.0"
redis = ">=4.5.3"
[package.extras]
cryptography = ["cryptography (>=1.3.0)"]
tests = ["async-timeout", "cryptography (>=1.3.0)", "pytest", "pytest-asyncio", "pytest-timeout"]
[[package]]
name = "chardet"
version = "3.0.4"
@ -3076,6 +3110,79 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "msgpack"
version = "1.0.5"
description = "MessagePack serializer"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"},
{file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"},
{file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"},
{file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"},
{file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"},
{file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"},
{file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"},
{file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"},
{file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"},
{file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"},
{file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"},
{file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"},
{file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"},
{file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"},
{file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"},
{file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"},
{file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"},
{file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"},
{file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"},
{file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"},
{file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"},
{file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"},
{file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"},
{file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"},
{file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"},
{file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"},
{file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"},
{file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"},
{file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"},
{file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"},
{file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"},
{file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"},
{file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"},
{file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"},
{file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"},
{file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"},
{file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"},
{file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"},
{file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"},
{file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"},
{file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"},
{file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"},
{file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"},
{file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"},
{file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"},
{file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"},
{file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"},
{file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"},
{file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"},
{file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"},
{file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"},
{file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"},
{file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"},
{file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"},
{file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"},
{file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"},
{file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"},
{file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"},
{file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"},
{file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"},
{file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"},
{file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"},
{file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"},
]
[[package]]
name = "multidict"
version = "6.0.4"
@ -3797,6 +3904,140 @@ files = [
{file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"},
]
[[package]]
name = "pydantic"
version = "2.0.2"
description = "Data validation using Python type hints"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-2.0.2-py3-none-any.whl", hash = "sha256:f5581e0c79b2ec2fa25a9d30d766629811cdda022107fa73d022ab5578873ae3"},
{file = "pydantic-2.0.2.tar.gz", hash = "sha256:b802f5245b8576315fe619e5989fd083448fa1258638ef9dac301ca60878396d"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.1.2"
typing-extensions = ">=4.6.1"
[package.extras]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.1.2"
description = ""
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic_core-2.1.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:b4815720c266e832b20e27a7a5f3772bb09fdedb31a9a34bab7b49d98967ef5a"},
{file = "pydantic_core-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8884a1dbfc5cb8c54b48446ca916d4577c1f4d901126091e4ab25d00194e065f"},
{file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a33aa69d476773230396396afb8e11908f8dafdcfd422e746770599a3f889d"},
{file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af832edd384755826e494ffdcf1fdda86e4babc42a0b18d342943fb18181040e"},
{file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:017700236ea2e7afbef5d3803559c80bd8720306778ebd49268de7ce9972e83e"},
{file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:c2d00a96fdf26295c6f25eaf9e4a233f353146a73713cd97a5f5dc6090c3aef2"},
{file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_24_s390x.whl", hash = "sha256:2575664f0a559a7b951a518f6f34c23cab7190f34f8220b8c8218c4f403147ee"},
{file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24c3c9180a2d19d640bacc2d00f497a9a1f2abadb2a9ee201b56bb03bc5343bd"},
{file = "pydantic_core-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:88a56f0f6d020b4d17641f4b4d1f9540a536d4146768d059c430e97bdb485fc1"},
{file = "pydantic_core-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fa38a76e832743866aed6b715869757074b06357d1a260163ec26d84974245fe"},
{file = "pydantic_core-2.1.2-cp310-none-win32.whl", hash = "sha256:a772c652603855d7180015849d483a1f539351a263bb9b81bfe85193a33ce124"},
{file = "pydantic_core-2.1.2-cp310-none-win_amd64.whl", hash = "sha256:b4673d1f29487608d613ebcc5caa99ba15eb58450a7449fb6d800f29d90bebc1"},
{file = "pydantic_core-2.1.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:76c9c55462740d728b344e3a087775846516c3fee31ec56e2075faa7cfcafcbf"},
{file = "pydantic_core-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb854ec52e6e2e05b83d647695f4d913452fdd45a3dfa8233d7dab5967b3908f"},
{file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac140d54da366672f6b91f9a1e8e2d4e7e72720143353501ae886d3fca03272"},
{file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818f5cb1b209ab1295087c45717178f4bbbd2bd7eda421f7a119e7b9b736a3cb"},
{file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:db4564aea8b3cb6cf1e5f3fd80f1ced73a255d492396d1bd8abd688795b34d63"},
{file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:2ca2d2d5ab65fb40dd05259965006edcc62a9d9b30102737c0a6f45bcbd254e8"},
{file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_24_s390x.whl", hash = "sha256:7c7ad8958aadfbcd664078002246796ecd5566b64b22f6af4fd1bbcec6bf8f60"},
{file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:080a7af828388284a68ad7d3d3eac3bcfff6a580292849aff087e7d556ec42d4"},
{file = "pydantic_core-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bad7029fb2251c1ac7d3acdd607e540d40d137a7d43a5e5acdcfdbd38db3fc0a"},
{file = "pydantic_core-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1635a37137fafbc6ee0a8c879857e05b30b1aabaa927e653872b71f1501b1502"},
{file = "pydantic_core-2.1.2-cp311-none-win32.whl", hash = "sha256:eb4301f009a44bb5db5edfe4e51a8175a4112b566baec07f4af8b1f8cb4649a2"},
{file = "pydantic_core-2.1.2-cp311-none-win_amd64.whl", hash = "sha256:ebf583f4d9b52abd15cc59e5f6eeca7e3e9741c6ea62d8711c00ac3acb067875"},
{file = "pydantic_core-2.1.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:90b06bb47e60173d24c7cb79670aa8dd6081797290353b9d3c66d3a23e88eb34"},
{file = "pydantic_core-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e5761ce986ec709897b1b965fad9743f301500434bea3cbab2b6e662571580f"},
{file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9f8bf1d7008a58fbb6eb334dc6e2f2905400cced8dadb46c4ca28f005a8562"},
{file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a014ee88980013d192a718cbb88e8cea20acd3afad69bc6d15672d05a49cdb6"},
{file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:8125152b03dd91deca5afe5b933a1994b39405adf6be2fe8dce3632319283f85"},
{file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_24_ppc64le.whl", hash = "sha256:dc737506b4a0ba2922a2626fc6d620ce50a46aebd0fe2fbcad1b93bbdd8c7e78"},
{file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_24_s390x.whl", hash = "sha256:bb471ea8650796060afc99909d9b75da583d317e52f660faf64c45f70b3bf1e2"},
{file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1fad38db1744d27061df516e59c5025b09b0a50a337c04e6eebdbddc18951bc"},
{file = "pydantic_core-2.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:94d368af9e6563de6e7170a74710a2cbace7a1e9c8e507d9e3ac34c7065d7ae3"},
{file = "pydantic_core-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd95d223de5162811a7b36c73d48eac4fee03b075132f3a1b73c132ce157a60c"},
{file = "pydantic_core-2.1.2-cp312-none-win32.whl", hash = "sha256:cd62f73830d4715bc643ae39de0bd4fb9c81d6d743530074da91e77a2cccfe67"},
{file = "pydantic_core-2.1.2-cp312-none-win_amd64.whl", hash = "sha256:51968887d6bd1eaa7fc7759701ea8ccb470c04654beaa8ede6835b0533f206a9"},
{file = "pydantic_core-2.1.2-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:7ff6bfe63f447a509ed4d368a7f4ba6a7abc03bc4744fc3fb30f2ffab73f3821"},
{file = "pydantic_core-2.1.2-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:4e67f9b9dfda2e42b39459cbf99d319ccb90da151e35cead3521975b2afbf673"},
{file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b815a769b019dd96be6571096f246b74f63330547e9b30244c51b4a2eb0277fc"},
{file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aff436c23c68449601b3fba7075b4f37ef8fbb893c8c1ed3ef898f090332b1e"},
{file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_24_armv7l.whl", hash = "sha256:2ee3ae58f271851362f6c9b33e4c9f9e866557ec7d8c03dc091e9b5aa5566cec"},
{file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cf92dccca8f66e987f6c4378700447f82b79e86407912ab1ee06b16b82f05120"},
{file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_24_s390x.whl", hash = "sha256:4663293a36a851a860b1299c50837914269fca127434911297dd39fea9667a01"},
{file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c917f7a41d9d09b8b024a5d65cf37e5588ccdb6e610d2df565fb7186b1f3b1c"},
{file = "pydantic_core-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:06ae67547251135a1b3f8dd465797b13146295a3866bc12ddd73f7512787bb7c"},
{file = "pydantic_core-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4938b32c09dbcecbeb652327cb4a449b1ef1a1bf6c8fc2c8241aa6b8f6d63b54"},
{file = "pydantic_core-2.1.2-cp37-none-win32.whl", hash = "sha256:682ff9228c838018c47dfa89b3d84cca45f88cacde28807ab8296ec221862af4"},
{file = "pydantic_core-2.1.2-cp37-none-win_amd64.whl", hash = "sha256:6e3bcb4a9bc209a61ea2aceb7433ce2ece32c7e670b0c06848bf870c9b3e7d87"},
{file = "pydantic_core-2.1.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:2278ca0b0dfbcfb1e12fa58570916dc260dc72bee5e6e342debf5329d8204688"},
{file = "pydantic_core-2.1.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87cff210af3258ca0c829e3ebc849d7981bfde23a99d6cb7a3c17a163b3dbad2"},
{file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7684b5fb906b37e940c5df3f57118f32e033af5e4770e5ae2ae56fbd2fe1a30a"},
{file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3747a4178139ebf3f19541285b2eb7c886890ca4eb7eec851578c02a13cc1385"},
{file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_24_armv7l.whl", hash = "sha256:e17056390068afd4583d88dcf4d4495764e4e2c7d756464468e0d21abcb8931e"},
{file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:c720e55cef609d50418bdfdfb5c44a76efc020ae7455505788d0113c54c7df55"},
{file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_24_s390x.whl", hash = "sha256:b59a64c367f350873c40a126ffe9184d903d2126c701380b4b55753484df5948"},
{file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68a2a767953c707d9575dcf14d8edee7930527ee0141a8bb612c22d1f1059f9a"},
{file = "pydantic_core-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae46769d9a7138d58cd190441cac14ce954010a0081f28462ed916c8e55a4f"},
{file = "pydantic_core-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc909f62325a631e1401dd07dfc386986dbcac15f98c9ff2145d930678a9d25a"},
{file = "pydantic_core-2.1.2-cp38-none-win32.whl", hash = "sha256:b4038869ba1d8fa33863b4b1286ab07e6075a641ae269b865f94d7e10b3e800e"},
{file = "pydantic_core-2.1.2-cp38-none-win_amd64.whl", hash = "sha256:5948af62f323252d56acaec8ebfca5f15933f6b72f8dbe3bf21ee97b2d10e3f0"},
{file = "pydantic_core-2.1.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:8e6ce261ccb9a986953c4dce070327e4954f9dd4cd214746dfc70efbc713b6a1"},
{file = "pydantic_core-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d35d634d9d1ed280c87bc2a7a6217b8787eedc86f368fc2fa1c0c8c78f7d3c93"},
{file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be2e2812a43205728a06c9d0fd090432cd76a9bb5bff2bfcfdf8b0e27d51851"},
{file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0eb54b11cd4fe0c6404611eef77086ade03eb1457e92910bbb4f3479efa3f79"},
{file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_24_armv7l.whl", hash = "sha256:087ddbb754575618a8832ee4ab52fe7eb332f502e2a56088b53dbeb5c4efdf9f"},
{file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b74906e01c7fc938ac889588ef438de812989817095c3c4904721f647d64a4d1"},
{file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_24_s390x.whl", hash = "sha256:60b7239206a2f61ad89c7518adfacb3ccd6662eaa07c5e437317aea2615a1f18"},
{file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:be3419204952bbe9b72b90008977379c52f99ae1c6e640488de4be783c345d71"},
{file = "pydantic_core-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:804cf8f6a859620f8eb754c02f7770f61c3e9c519f8338c331d555b3d6976e3c"},
{file = "pydantic_core-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cbba32fb14e199d0493c6b9c44870dab0a9c37af9f0f729068459d1849279ffd"},
{file = "pydantic_core-2.1.2-cp39-none-win32.whl", hash = "sha256:6bf00f56a4468f5b03dadb672a5f1d24aea303d4ccffe8a0f548c9e36017edd3"},
{file = "pydantic_core-2.1.2-cp39-none-win_amd64.whl", hash = "sha256:ac462a28218ea7d592c7ad51b517558f4ac6565a4e53db7a4811eeaf9c9660b0"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:047e782b9918f35ef534ced36f1fd2064f5581229b7a15e4d3177387a6b53134"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0213891898fa5b404cf3edf4797e3ac7819a0708ea5473fc6432a2aa27c189"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0f481aaf0119f77b200e5a5e2799b3e14c015a317eaa948f42263908735cc9f"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15eb4cb543ed36f6a4f16e3bee7aa7ed1c3757be95a3f3bbb2b82b9887131e0f"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ef71e73a81a4cd7e87c93e8ff0170140fd93ba33b0f61e83da3f55f6e0a84fb4"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:840238c845b0f80777151fef0003088ab91c6f7b3467edaff4932b425c4e3c3f"},
{file = "pydantic_core-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7648e48ba263ca0a8a2dc55a60a219c9133fb101ba52c89a14a29fb3d4322ca3"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:8eb4e2b71562375609c66a79f89acd4fe95c5cba23473d04952c8b14b6f908f5"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056afea59651c4e47ec6dadbb77ccae4742c059a3d12bc1c0e393d189d2970d"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46cd323371aa7e4053010ccdb94063a4273aa9e5dbe97f8a1147faa769de8d8d"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa39499625239da4ec960cf4fc66b023929b24cc77fb8520289cfdb3c1986428"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f5de2d4167fd4bc5ad205fb7297e25867b8e335ca08d64ed7a561d2955a2c32d"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9a5fba9168fc27805553760fa8198db46eef83bf52b4e87ebbe1333b823d0e70"},
{file = "pydantic_core-2.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e68a404fad8493989d6f07b7b9e066f1d2524d7cb64db2d4e9a84c920032c67f"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:1a5c4475510d1a9cc1458a26cfc21442223e52ce9adb640775c38739315d03c7"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0681472245ef182554208a25d16884c84f1c5a69f14e6169b88932e5da739a1c"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7fd334b40c5e13a97becfcaba314de0dcc6f7fe21ec8f992139bcc64700e9dc"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7345b1741bf66a9d8ed0ec291c3eabd534444e139e1ea6db5742ac9fd3be2530"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0855cf8b760fb40f97f0226cb527c8a94a2ab9d8179628beae20d6939aaeacb0"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d281a10837d98db997c0247f45d138522c91ce30cf3ae7a6afdb5e709707d360"},
{file = "pydantic_core-2.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:82e09f27edab289187dd924d4d93f2a35f21aa969699b2504aa643da7fbfeff9"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:aa54902fa51f7d921ba80923cf1c7ff3dce796a7903300bd8824deb90e357744"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b9a5fc4058d64c9c826684dcdb43891c1b474a4a88dcf8dfc3e1fb5889496f8"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:817681d111cb65f07d46496eafec815f48e1aff37713b73135a0a9eb4d3610ab"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b5d37aedea5963f2097bddbcdb255483191646a52d40d8bb66d61c190fcac91"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f2de65752fff248319bcd3b29da24e205fa505607539fcd4acc4037355175b63"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:a8b9c2cc4c5f8169b943d24be4bd1548fe81c016d704126e3a3124a2fc164885"},
{file = "pydantic_core-2.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f7bcdf70c8b6e70be11c78d3c00b80a24cccfb408128f23e91ec3019bed1ecc1"},
{file = "pydantic_core-2.1.2.tar.gz", hash = "sha256:d2c790f0d928b672484eac4f5696dd0b78f3d6d148a641ea196eb49c0875e30a"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydotplus"
version = "2.0.2"
@ -5650,4 +5891,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "538c0233deaa23ceee9a4b0dd547ffd759317b54cd5908129a9786c724a696bd"
content-hash = "d06b2a1ccbc8c661df8bc425ccab27516a6dfea1b550976d1338292b47ce8f83"

View File

@ -91,6 +91,8 @@ django-tables2 = "^2.5.3"
django-filter = "^23.2"
tablib = "^3.4.0"
django-location-field = "^2.7.0"
pydantic = "^2.0.2"
channels-redis = "^4.1.0"
[build-system]