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.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
from akarpov.common.jwt import read_jwt 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"])) scope["user"] = await get_user(dict(scope["headers"]))
return await self.app(scope, receive, send) 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.apps import AppConfig
from django.core.exceptions import AppRegistryNotReady
class MusicConfig(AppConfig): class MusicConfig(AppConfig):
@ -10,3 +11,12 @@ def ready(self):
import akarpov.music.signals # noqa F401 import akarpov.music.signals # noqa F401
except ImportError: except ImportError:
pass 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) name = models.CharField(blank=True, max_length=250)
status = models.CharField(null=True, blank=True, max_length=250) status = models.CharField(null=True, blank=True, max_length=250)
error = models.BooleanField(default=False) 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 import os
from django.core.files import File 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 mutagen.mp3 import MP3
from PIL import Image from PIL import Image
from pydub import AudioSegment from pydub import AudioSegment
@ -15,10 +17,11 @@ def load_track(
album: str | None = None, album: str | None = None,
name: str | None = None, name: str | None = None,
link: str | None = None, link: str | None = None,
**kwargs,
) -> Song: ) -> Song:
p_name = path.split("/")[-1] p_name = path.split("/")[-1]
if authors: 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: else:
authors = [] authors = []
if album: if album:
@ -27,7 +30,9 @@ def load_track(
album = None album = None
if sng := Song.objects.filter( 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() return sng.first()
@ -37,8 +42,7 @@ def load_track(
os.remove(path) os.remove(path)
path = mp3_path path = mp3_path
audio = MP3(path) tag = MP3(path, ID3=ID3)
if image_path: if image_path:
if not image_path.endswith(".png"): if not image_path.endswith(".png"):
im = Image.open(image_path) im = Image.open(image_path)
@ -46,18 +50,50 @@ def load_track(
im.save(image_path) im.save(image_path)
song = Song( 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: if image_path:
with open(image_path, "rb") as file: with open(path, "rb") as file, open(image_path, "rb") as image:
song.image = File(file, name=image_path.split("/")[-1]) 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: if authors:
song.authors.set(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 return song

View File

@ -1,49 +1,60 @@
import os
from io import BytesIO
from pathlib import Path from pathlib import Path
from random import randint
import mutagen 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): def load_dir(path: str):
path = Path(path) path = Path(path)
for f in list(path.glob("**/*.mp3")): for f in list(path.glob("**/*.mp3")):
with f.open("rb") as file: process_mp3_file(str(f))
process_mp3_file(File(file, name=str(f).split("/")[-1]), str(f))
def load_file(path: str): def load_file(path: str):
with open(path, "rb") as file: process_mp3_file(path)
process_mp3_file(File(file, name=path.split("/")[-1]), path)
def process_mp3_file(file: File, path: str) -> None: def process_mp3_file(path: str) -> None:
que = SongInQue.objects.create() tag = mutagen.File(path, easy=True)
try: if "artist" in tag:
tag = mutagen.File(path, easy=True) author = tag["artist"]
que.name = tag["title"][0] if "title" in tag else path.split("/")[-1] else:
que.save() author = None
if "artist" in tag:
author = Author.objects.get_or_create(name=tag["artist"][0])[0]
else:
author = None
if "album" in tag: if "album" in tag:
album = Album.objects.get_or_create(name=tag["album"][0])[0] album = tag["album"]
else: else:
album = None 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( tags = ID3(path)
name=tag["title"][0] if "title" in tag else path.split("/")[-1], pict = [x for x in tags.getall("APIC") if x]
author=author, image_pth = None
album=album, if pict:
) try:
song.file = file pict = pict[0].data
song.save(update_fields=["file"]) im = Image.open(BytesIO(pict))
que.delete() image_pth = f"/tmp/{randint(1, 1000000)}.png"
except Exception as e: if os.path.exists(image_pth):
que.name = e image_pth = f"/tmp/{randint(1, 1000000)}.png"
que.error = True im.save(image_pth)
que.save() 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 import os
from pathlib import Path
from random import randint from random import randint
import requests import requests
from django.conf import settings 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 PIL import Image
from pydub import AudioSegment from pydub import AudioSegment
from pytube import Search, YouTube 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 from akarpov.music.services.spotify import get_track_info
@ -37,9 +33,6 @@ def download_from_youtube_link(link: str) -> Song:
que.delete() que.delete()
return sng.first() 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() audio = yt.streams.filter(only_audio=True).order_by("abr").desc().first()
orig_path = audio.download(output_path=settings.MEDIA_ROOT) 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"]) r = requests.get(info["album_image"])
img_pth = str( img_pth = str(
settings.MEDIA_ROOT 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: with open(img_pth, "wb") as f:
f.write(r.content) f.write(r.content)
@ -62,41 +55,9 @@ def download_from_youtube_link(link: str) -> Song:
os.remove(img_pth) os.remove(img_pth)
# set music meta load_track(path, img_pth, info["artists"], info["album_name"])
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
except Exception as e: except Exception as e:
print(e)
que.name = e que.name = e
que.error = True que.error = True
que.save() que.save()

View File

@ -1,8 +1,13 @@
from asgiref.sync import async_to_sync
from celery import shared_task from celery import shared_task
from channels.layers import get_channel_layer
from pytube import Channel, Playlist 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 import yandex, youtube
from akarpov.music.services.file import load_dir, load_file from akarpov.music.services.file import load_dir, load_file
from akarpov.utils.celery import get_scheduled_tasks_name
@shared_task @shared_task
@ -44,3 +49,39 @@ def process_file(path):
@shared_task @shared_task
def load_ym_file_meta(track): def load_ym_file_meta(track):
return yandex.load_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("album/<str:slug>", views.album_view, name="album"),
path("author/<str:slug>", views.author_view, name="author"), path("author/<str:slug>", views.author_view, name="author"),
path("playlist/<str:slug>", views.playlist_view, name="playlist"), 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): class LoadTrackView(SuperUserRequiredMixin, generic.FormView):
form_class = TracksLoadForm form_class = TracksLoadForm
template_name = "music/upload.html"
def get_success_url(self): def get_success_url(self):
# TODO: add room to see tracks load # TODO: add room to see tracks load
return "" return ""
def form_valid(self, form): def form_valid(self, form):
load_tracks(form.address) load_tracks(form.data["address"])
return super().form_valid(form) return super().form_valid(form)
@ -76,3 +77,10 @@ def form_valid(self, form):
load_track_file_view = LoadTrackFileView.as_view() 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 import functools
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
def login_required(func): def login_required(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if not self.scope.get("user", False) or not self.scope["user"].is_authenticated: if not self.scope.get("user", False) or not self.scope["user"].is_authenticated:
self.send_error("Требуется авторизация") self.send_error("Auth is required")
else: else:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return wrapper 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 import os
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf import settings
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
from akarpov.common.channels import HeaderAuthMiddleware from akarpov.common.channels import HeaderAuthMiddleware
from config import routing 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( 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" WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application" ASGI_APPLICATION = "config.asgi.application"
# CHANNELS
# ------------------------------------------------------------------------------
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
# APPS # APPS
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DJANGO_APPS = [ DJANGO_APPS = [

243
poetry.lock generated
View File

@ -180,6 +180,18 @@ imageio = ">=1.5"
numpy = ">=1.11.1" numpy = ">=1.11.1"
Pillow = ">=3.3.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]] [[package]]
name = "anyio" name = "anyio"
version = "3.7.1" version = "3.7.1"
@ -726,6 +738,28 @@ Django = ">=3.2"
daphne = ["daphne (>=4.0.0)"] daphne = ["daphne (>=4.0.0)"]
tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"] 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]] [[package]]
name = "chardet" name = "chardet"
version = "3.0.4" version = "3.0.4"
@ -3076,6 +3110,79 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, {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]] [[package]]
name = "multidict" name = "multidict"
version = "6.0.4" version = "6.0.4"
@ -3797,6 +3904,140 @@ files = [
{file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"}, {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]] [[package]]
name = "pydotplus" name = "pydotplus"
version = "2.0.2" version = "2.0.2"
@ -5650,4 +5891,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" 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" django-filter = "^23.2"
tablib = "^3.4.0" tablib = "^3.4.0"
django-location-field = "^2.7.0" django-location-field = "^2.7.0"
pydantic = "^2.0.2"
channels-redis = "^4.1.0"
[build-system] [build-system]