mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 14:26:34 +03:00
fixed music load, added radio
This commit is contained in:
parent
72f17c8ee2
commit
bb61d144c5
|
@ -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}})
|
||||||
|
|
44
akarpov/music/api/serializers.py
Normal file
44
akarpov/music/api/serializers.py
Normal 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",
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
51
akarpov/music/consumers.py
Normal file
51
akarpov/music/consumers.py
Normal 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)
|
37
akarpov/music/migrations/0004_radiosong.py
Normal file
37
akarpov/music/migrations/0004_radiosong.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
akarpov/music/migrations/0005_radiosong_start.py
Normal file
18
akarpov/music/migrations/0005_radiosong_start.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if image_path:
|
||||||
|
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:
|
with open(path, "rb") as file:
|
||||||
song.file = File(file, name=path.split("/")[-1])
|
song.file = File(file, name=path.split("/")[-1])
|
||||||
|
song.save()
|
||||||
if image_path:
|
|
||||||
with open(image_path, "rb") as file:
|
|
||||||
song.image = File(file, name=image_path.split("/")[-1])
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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()
|
|
||||||
try:
|
|
||||||
tag = mutagen.File(path, easy=True)
|
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:
|
if "artist" in tag:
|
||||||
author = Author.objects.get_or_create(name=tag["artist"][0])[0]
|
author = tag["artist"]
|
||||||
else:
|
else:
|
||||||
author = None
|
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
330
akarpov/templates/music/radio.html
Normal file
330
akarpov/templates/music/radio.html
Normal 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 %}
|
20
akarpov/templates/music/upload.html
Normal file
20
akarpov/templates/music/upload.html
Normal 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
10
akarpov/utils/celery.py
Normal 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]
|
|
@ -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}})
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||||
|
else:
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
|
||||||
|
|
||||||
application = ProtocolTypeRouter(
|
application = ProtocolTypeRouter(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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()),
|
||||||
|
]
|
||||||
|
|
|
@ -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
243
poetry.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user