mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-10 21:56:34 +03:00
Compare commits
5 Commits
c52baa05f8
...
3385c99d80
Author | SHA1 | Date | |
---|---|---|---|
|
3385c99d80 | ||
dba8bc9ba4 | |||
cb7bbbf49f | |||
fb2b611dd8 | |||
017efcb1cc |
|
@ -9,3 +9,9 @@ SENTRY_DSN=
|
|||
EMAIL_PASSWORD=
|
||||
EMAIL_USER=
|
||||
EMAIL_USE_SSL=false
|
||||
|
||||
LAST_FM_API_KET=
|
||||
LAST_FM_SECRET=
|
||||
SPOTIFY_ID=
|
||||
SPOTIFY_SECRET=
|
||||
YANDEX_TOKEN=
|
||||
|
|
96
about_page/about.html
Normal file
96
about_page/about.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neofetch in HTML</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Fira+Mono:500" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e1e;
|
||||
color: #c7c7c7;
|
||||
font-family: 'Fira Mono', monospace;
|
||||
margin: 0;
|
||||
padding:
|
||||
|
||||
20px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
.neofetch-container {
|
||||
align-items: start;
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(auto, 1fr) 3fr;
|
||||
}
|
||||
.neofetch-logo img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
grid-row: 1;
|
||||
}
|
||||
.neofetch-info {
|
||||
white-space: pre;
|
||||
line-height: 1.1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.color-blocks {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.color-block {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.red { background-color: #FF5555; }
|
||||
.green { background-color: #50FA7B; }
|
||||
.yellow { background-color: #F1FA8C; }
|
||||
.blue { background-color: #BD93F9; }
|
||||
.purple { background-color: #FF79C6; }
|
||||
.cyan { background-color: #8BE9FD; }
|
||||
.white { background-color: #BBBBBB; }
|
||||
.black { background-color: #282A36; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<divЮ class="neofetch-container">
|
||||
<div class="neofetch-logo">
|
||||
<img src="archlinux-icon.svg" alt="Arch Linux Logo">
|
||||
</div>
|
||||
<div class="neofetch-info">
|
||||
<span class="key-name">sanspie@TanOS</span>
|
||||
<br>
|
||||
-------------<br>
|
||||
<span class="key-name">OS:</span> ArchLinux x86_64<br>
|
||||
<span class="key-name">Kernel:</span> 6.6.10-arch1-1<br>
|
||||
<span class="key-name">Uptime:</span> 59 mins<br>
|
||||
<span class="key-name">Packages:</span> 2097 (pacman)<br>
|
||||
<span class="key-name">Shell:</span> zsh 5.9<br>
|
||||
<span class="key-name">Resolution:</span> 1920x1080, 3440x1440<br>
|
||||
<span class="key-name">DE:</span> Plasma 5.27.10<br>
|
||||
<span class="key-name">WM:</span> KWin<br>
|
||||
<span class="key-name">WM Theme:</span> Sweet-Dark<br>
|
||||
<span class="key-name">Theme:</span> [Plasma], Breeze [GTK2/3]<br>
|
||||
<span class="key-name">Icons:</span> candy-icons [Plasma], candy-icons [GTK2/3]<br>
|
||||
<span class="key-name">Terminal:</span> alacritty<br>
|
||||
<span class="key-name">CPU:</span> AMD Ryzen 7 3700X (16) @ 3.600GHz<br>
|
||||
<span class="key-name">GPU:</span> NVIDIA GeForce RTX 3060 Lite Hash Rate<br>
|
||||
<span class="key-name">Memory:</span> 12384MiB / 32022MiB<br>
|
||||
<span class="key-name">Disk (/):</span> 351G / 466G (76%)<br>
|
||||
</div>
|
||||
<div class="color-blocks">
|
||||
<div class="color-block black"></div> <!-- Add a black color block if needed -->
|
||||
<div class="color-block red"></div>
|
||||
<div class="color-block green"></div>
|
||||
<div class="color-block yellow"></div>
|
||||
<div class="color-block blue"></div>
|
||||
<div class="color-block purple"></div>
|
||||
<div class="color-block cyan"></div>
|
||||
<div class="color-block white"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
about_page/archlinux-icon.svg
Normal file
1
about_page/archlinux-icon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M31.994-.006c-2.85 6.985-4.568 11.554-7.74 18.332 1.945 2.062 4.332 4.462 8.2 7.174-4.168-1.715-7-3.437-9.136-5.224-4.06 8.47-10.42 20.538-23.327 43.73C10.145 58.15 18 54.54 25.338 53.16c-.315-1.354-.494-2.818-.48-4.345l.012-.325c.16-6.5 3.542-11.498 7.547-11.158s7.118 5.886 6.957 12.386a18.36 18.36 0 0 1-.409 3.491c7.25 1.418 15.03 5.02 25.037 10.797l-5.42-10.026c-2.65-2.053-5.413-4.726-11.05-7.62 3.875 1.007 6.65 2.168 8.8 3.467-17.1-31.84-18.486-36.07-24.35-49.833z" fill="#1793d1" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 589 B |
24
akarpov/blog/migrations/0010_alter_tag_color.py
Normal file
24
akarpov/blog/migrations/0010_alter_tag_color.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.2.8 on 2024-01-14 12:06
|
||||
|
||||
import colorfield.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("blog", "0009_alter_comment_parent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="tag",
|
||||
name="color",
|
||||
field=colorfield.fields.ColorField(
|
||||
blank=True,
|
||||
default="#FF0000",
|
||||
image_field=None,
|
||||
max_length=25,
|
||||
samples=None,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -13,6 +13,7 @@
|
|||
SongUserRating,
|
||||
TempFileUpload,
|
||||
UserListenHistory,
|
||||
UserMusicProfile,
|
||||
)
|
||||
|
||||
|
||||
|
@ -147,3 +148,5 @@ class UserListenHistoryAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
admin.site.register(UserListenHistory, UserListenHistoryAdmin)
|
||||
|
||||
admin.site.register(UserMusicProfile)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from django.db.models import Q
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
|
@ -65,6 +66,7 @@ class ListSongSerializer(SetUserModelSerializer):
|
|||
album = serializers.SerializerMethodField(method_name="get_album")
|
||||
authors = serializers.SerializerMethodField(method_name="get_authors")
|
||||
liked = serializers.SerializerMethodField(method_name="get_liked")
|
||||
image_cropped = serializers.SerializerMethodField(method_name="get_image")
|
||||
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
def get_liked(self, obj):
|
||||
|
@ -88,6 +90,25 @@ def get_authors(self, obj):
|
|||
).data
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.ImageField)
|
||||
def get_image(self, obj):
|
||||
img = None
|
||||
if obj.image_cropped:
|
||||
img = obj.image_cropped
|
||||
else:
|
||||
album = Album.objects.cache().get(id=obj.album_id)
|
||||
if album.image_cropped:
|
||||
img = album.image_cropped
|
||||
else:
|
||||
authors = Author.objects.cache().filter(
|
||||
Q(songs__id=obj.id) & ~Q(image="")
|
||||
)
|
||||
if authors:
|
||||
img = authors.first().image_cropped
|
||||
if img:
|
||||
return self.context["request"].build_absolute_uri(img.url)
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = Song
|
||||
fields = [
|
||||
|
|
46
akarpov/music/migrations/0015_usermusicprofile.py
Normal file
46
akarpov/music/migrations/0015_usermusicprofile.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 4.2.8 on 2024-01-14 12:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("music", "0014_alter_album_authors"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserMusicProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"lastfm_username",
|
||||
models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
(
|
||||
"lastfm_token",
|
||||
models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
("preferences", models.JSONField(null=True)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="music_profile",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -161,3 +161,15 @@ class UserListenHistory(models.Model):
|
|||
|
||||
class Meta:
|
||||
ordering = ["-created"]
|
||||
|
||||
|
||||
class UserMusicProfile(models.Model):
|
||||
user = models.OneToOneField(
|
||||
"users.User", related_name="music_profile", on_delete=models.CASCADE
|
||||
)
|
||||
lastfm_username = models.CharField(max_length=50, blank=True, null=True)
|
||||
lastfm_token = models.CharField(max_length=50, blank=True, null=True)
|
||||
preferences = models.JSONField(null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} music profile"
|
||||
|
|
|
@ -166,28 +166,34 @@ def get_yandex_artist_info(artist_name: str, client: Client):
|
|||
|
||||
|
||||
def download_image(url, save_path):
|
||||
image_path = os.path.join(save_path, f"tmp_{randint(10000, 99999)}.png")
|
||||
if type(url) is Cover:
|
||||
url = url["uri"]
|
||||
if not str(url).startswith("http"):
|
||||
url = "https://" + str(url)
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
image_path = os.path.join(save_path, f"tmp_{randint(10000, 99999)}.png")
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
return image_path
|
||||
url.download(image_path)
|
||||
else:
|
||||
if not url.startswith("http"):
|
||||
url = "https://" + url
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
return image_path
|
||||
return ""
|
||||
|
||||
|
||||
def update_album_info(album: AlbumModel, track_name: str) -> None:
|
||||
def update_album_info(album: AlbumModel, author_name: str = None) -> None:
|
||||
client = yandex_login()
|
||||
spotify_session = create_spotify_session()
|
||||
|
||||
# Retrieve info from both services
|
||||
yandex_album_info = get_yandex_album_info(album.name + " - " + track_name, client)
|
||||
spotify_album_info = get_spotify_album_info(
|
||||
album.name + " - " + track_name, spotify_session
|
||||
)
|
||||
if author_name:
|
||||
yandex_album_info = get_yandex_album_info(
|
||||
album.name + " - " + author_name, client
|
||||
)
|
||||
spotify_album_info = get_spotify_album_info(
|
||||
album.name + " - " + author_name, spotify_session
|
||||
)
|
||||
else:
|
||||
yandex_album_info = get_yandex_album_info(album.name, client)
|
||||
spotify_album_info = get_spotify_album_info(album.name, spotify_session)
|
||||
|
||||
# Combine and prioritize Spotify data
|
||||
album_data = {}
|
||||
|
@ -246,6 +252,7 @@ def update_album_info(album: AlbumModel, track_name: str) -> None:
|
|||
save=True,
|
||||
)
|
||||
os.remove(image_path)
|
||||
album.save()
|
||||
|
||||
# Update Album Authors from Spotify data if available
|
||||
if spotify_album_info and "artists" in spotify_album_info:
|
||||
|
@ -271,17 +278,13 @@ def update_album_info(album: AlbumModel, track_name: str) -> None:
|
|||
album.save()
|
||||
|
||||
|
||||
def update_author_info(author: Author, track_name: str) -> None:
|
||||
def update_author_info(author: Author) -> None:
|
||||
client = yandex_login()
|
||||
spotify_session = create_spotify_session()
|
||||
|
||||
# Retrieve info from both services
|
||||
yandex_artist_info = get_yandex_artist_info(
|
||||
author.name + " - " + track_name, client
|
||||
)
|
||||
spotify_artist_info = get_spotify_artist_info(
|
||||
author.name + " - " + track_name, spotify_session
|
||||
)
|
||||
yandex_artist_info = get_yandex_artist_info(author.name, client)
|
||||
spotify_artist_info = get_spotify_artist_info(author.name, spotify_session)
|
||||
|
||||
# Combine and prioritize Spotify data
|
||||
author_data = {}
|
||||
|
@ -332,6 +335,7 @@ def update_author_info(author: Author, track_name: str) -> None:
|
|||
save=True,
|
||||
)
|
||||
os.remove(image_path)
|
||||
author.save()
|
||||
|
||||
if generated_name and not Author.objects.filter(slug=generated_name).exists():
|
||||
if len(generated_name) > 20:
|
||||
|
|
|
@ -101,6 +101,46 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
|
|||
chapter_path = path.split(".")[0] + chapters[i][2] + ".mp3"
|
||||
info = search_all_platforms(chapters[i][2])
|
||||
audio.export(chapter_path, format="mp3")
|
||||
if not info["album_image"].startswith("/"):
|
||||
r = requests.get(info["album_image"])
|
||||
img_pth = str(
|
||||
settings.MEDIA_ROOT
|
||||
+ f"/{info['album_image'].split('/')[-1]}_{str(randint(100, 999))}"
|
||||
)
|
||||
with open(img_pth, "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
im = Image.open(img_pth)
|
||||
im.save(str(f"{img_pth}.png"))
|
||||
img_pth = f"{img_pth}.png"
|
||||
else:
|
||||
img_pth = info["album_image"]
|
||||
|
||||
if "genre" in info:
|
||||
song = load_track(
|
||||
chapter_path,
|
||||
img_pth,
|
||||
user_id,
|
||||
info["artists"],
|
||||
info["album_name"],
|
||||
chapters[i][2],
|
||||
genre=info["genre"],
|
||||
)
|
||||
else:
|
||||
song = load_track(
|
||||
chapter_path,
|
||||
img_pth,
|
||||
user_id,
|
||||
info["artists"],
|
||||
info["album_name"],
|
||||
chapters[i][2],
|
||||
)
|
||||
os.remove(chapter_path)
|
||||
else:
|
||||
print(f"[processing] loading {title}")
|
||||
|
||||
info = search_all_platforms(title)
|
||||
if not info["album_image"].startswith("/"):
|
||||
r = requests.get(info["album_image"])
|
||||
img_pth = str(
|
||||
settings.MEDIA_ROOT
|
||||
|
@ -113,46 +153,13 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
|
|||
im.save(str(f"{img_pth}.png"))
|
||||
|
||||
os.remove(img_pth)
|
||||
if "genre" in info:
|
||||
song = load_track(
|
||||
chapter_path,
|
||||
f"{img_pth}.png",
|
||||
user_id,
|
||||
info["artists"],
|
||||
info["album_name"],
|
||||
chapters[i][2],
|
||||
genre=info["genre"],
|
||||
)
|
||||
else:
|
||||
song = load_track(
|
||||
chapter_path,
|
||||
f"{img_pth}.png",
|
||||
user_id,
|
||||
info["artists"],
|
||||
info["album_name"],
|
||||
chapters[i][2],
|
||||
)
|
||||
os.remove(chapter_path)
|
||||
else:
|
||||
print(f"[processing] loading {title}")
|
||||
|
||||
info = search_all_platforms(title)
|
||||
r = requests.get(info["album_image"])
|
||||
img_pth = str(
|
||||
settings.MEDIA_ROOT
|
||||
+ f"/{info['album_image'].split('/')[-1]}_{str(randint(100, 999))}"
|
||||
)
|
||||
with open(img_pth, "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
im = Image.open(img_pth)
|
||||
im.save(str(f"{img_pth}.png"))
|
||||
|
||||
os.remove(img_pth)
|
||||
img_pth = f"{img_pth}.png"
|
||||
else:
|
||||
img_pth = info["album_image"]
|
||||
if "genre" in info:
|
||||
song = load_track(
|
||||
path,
|
||||
f"{img_pth}.png",
|
||||
img_pth,
|
||||
user_id,
|
||||
info["artists"],
|
||||
info["album_name"],
|
||||
|
@ -162,7 +169,7 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
|
|||
else:
|
||||
song = load_track(
|
||||
path,
|
||||
f"{img_pth}.png",
|
||||
img_pth,
|
||||
user_id,
|
||||
info["artists"],
|
||||
info["album_name"],
|
||||
|
|
|
@ -17,15 +17,14 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
|
|||
@receiver(post_save, sender=Author)
|
||||
def author_create(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
songs = Song.objects.filter(authors=instance)
|
||||
update_author_info(instance, songs.first().name if songs.exists() else "")
|
||||
update_author_info(instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Album)
|
||||
def album_create(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
songs = Song.objects.filter(album=instance)
|
||||
update_album_info(instance, songs.first().name if songs.exists() else "")
|
||||
authors = instance.authors.all()
|
||||
update_album_info(instance, authors.first().name if authors.exists() else None)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import pylast
|
||||
import structlog
|
||||
from asgiref.sync import async_to_sync
|
||||
from celery import shared_task
|
||||
from channels.layers import get_channel_layer
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import now
|
||||
from pytube import Channel, Playlist
|
||||
|
||||
from akarpov.music.api.serializers import SongSerializer
|
||||
from akarpov.music.models import RadioSong, Song, UserListenHistory
|
||||
from akarpov.music.models import RadioSong, Song, UserListenHistory, UserMusicProfile
|
||||
from akarpov.music.services import yandex, youtube
|
||||
from akarpov.music.services.file import load_dir, load_file
|
||||
from akarpov.utils.celery import get_scheduled_tasks_name
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def list_tracks(url, user_id):
|
||||
|
@ -105,11 +111,31 @@ def listen_to_song(song_id, user_id=None):
|
|||
if (
|
||||
last_listen
|
||||
and last_listen.song_id == song_id
|
||||
or last_listen.created + s.length > now()
|
||||
or last_listen
|
||||
and last_listen.created + s.length > now()
|
||||
):
|
||||
return
|
||||
UserListenHistory.objects.create(
|
||||
user_id=user_id,
|
||||
song_id=song_id,
|
||||
)
|
||||
try:
|
||||
user_profile = UserMusicProfile.objects.get(user_id=user_id)
|
||||
lastfm_token = user_profile.lastfm_token
|
||||
|
||||
# Initialize Last.fm network with the user's session key
|
||||
network = pylast.LastFMNetwork(
|
||||
api_key=settings.LAST_FM_API_KEY,
|
||||
api_secret=settings.LAST_FM_SECRET,
|
||||
session_key=lastfm_token,
|
||||
)
|
||||
song = Song.objects.get(id=song_id)
|
||||
artist_name = song.artists_names
|
||||
track_name = song.name
|
||||
timestamp = int(timezone.now().timestamp())
|
||||
network.scrobble(artist=artist_name, title=track_name, timestamp=timestamp)
|
||||
except UserMusicProfile.DoesNotExist:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Last.fm scrobble error: {e}")
|
||||
return song_id
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
app_name = "music"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.music_landing, name="landing"),
|
||||
path("upload", views.load_track_view, name="load"),
|
||||
path("upload_file", views.load_track_file_view, name="upload"),
|
||||
path("<str:slug>", views.song_view, name="song"),
|
||||
|
@ -13,4 +14,6 @@
|
|||
path("playlist/<str:slug>", views.playlist_view, name="playlist"),
|
||||
path("radio/", views.radio_main_view, name="radio"),
|
||||
path("player/", views.music_player_view, name="player"),
|
||||
path("lastfm/callback", views.lastfm_callback, name="lastfm_callback"),
|
||||
path("lastfm/connect", views.lastfm_auth, name="lastfm_connect"),
|
||||
]
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
import pylast
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
|
||||
from akarpov.common.views import SuperUserRequiredMixin
|
||||
from akarpov.music.forms import FileUploadForm, TracksLoadForm
|
||||
from akarpov.music.models import Album, Author, Playlist, Song, TempFileUpload
|
||||
from akarpov.music.models import (
|
||||
Album,
|
||||
Author,
|
||||
Playlist,
|
||||
Song,
|
||||
TempFileUpload,
|
||||
UserMusicProfile,
|
||||
)
|
||||
from akarpov.music.services.base import load_track_file, load_tracks
|
||||
|
||||
|
||||
|
@ -97,3 +111,68 @@ def get_queryset(self):
|
|||
|
||||
|
||||
music_player_view = MusicPlayerView.as_view()
|
||||
|
||||
|
||||
@login_required
|
||||
def lastfm_auth(request):
|
||||
API_KEY = settings.LAST_FM_API_KEY
|
||||
|
||||
if not API_KEY:
|
||||
raise Exception("LAST_FM_API_KEY not set in settings")
|
||||
|
||||
callback_url = (
|
||||
f"https://{get_current_site(request).domain}{reverse('music:lastfm_callback')}"
|
||||
)
|
||||
auth_url = f"http://www.last.fm/api/auth/?api_key={API_KEY}&cb={callback_url}"
|
||||
|
||||
return redirect(auth_url)
|
||||
|
||||
|
||||
@login_required
|
||||
def lastfm_callback(request):
|
||||
API_KEY = settings.LAST_FM_API_KEY
|
||||
API_SECRET = settings.LAST_FM_SECRET
|
||||
|
||||
token = request.GET.get("token")
|
||||
if not token:
|
||||
messages.error(request, "No token provided by Last.fm")
|
||||
return redirect(reverse("music:landing"))
|
||||
|
||||
network = pylast.LastFMNetwork(api_key=API_KEY, api_secret=API_SECRET)
|
||||
skg = pylast.SessionKeyGenerator(network)
|
||||
|
||||
try:
|
||||
session_key = skg.get_web_auth_session_key(url="", token=token)
|
||||
|
||||
user = request.user
|
||||
UserMusicProfile.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
"lastfm_token": session_key,
|
||||
"lastfm_username": user.username,
|
||||
},
|
||||
)
|
||||
messages.success(request, "Last.fm account is successfully connected")
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request, f"There was an error connecting your Last.fm account: {e}"
|
||||
)
|
||||
|
||||
return redirect(reverse("music:landing"))
|
||||
|
||||
|
||||
class MusicLanding(generic.TemplateView):
|
||||
template_name = "music/landing.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
if self.request.user.is_authenticated:
|
||||
try:
|
||||
kwargs["last_fm_account"] = UserMusicProfile.objects.get(
|
||||
user=self.request.user
|
||||
).lastfm_username
|
||||
except UserMusicProfile.DoesNotExist:
|
||||
kwargs["last_fm_account"] = None
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
music_landing = MusicLanding.as_view()
|
||||
|
|
|
@ -62,6 +62,10 @@
|
|||
<a href="{% url 'about' %}" class="{% active_link 'about' %} text-muted nav-link px-sm-0 px-2">
|
||||
<i class="fs-5 bi-person"></i><span class="ms-1 d-none d-sm-inline">About me</span></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'music:landing' %}" class="{% active_link 'music:landing' %} text-muted nav-link px-sm-0 px-2">
|
||||
<i class="fs-5 bi-music-note-list"></i><span class="ms-1 d-none d-sm-inline">Music</span></a>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'files:main' %}" class="{% active_link 'files:main' %} text-muted nav-link px-sm-0 px-2">
|
||||
|
|
15
akarpov/templates/music/landing.html
Normal file
15
akarpov/templates/music/landing.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Welcome to music app</h1>
|
||||
<p>This is mainly the backend of music, you should consider using side clients like: <a href="https://next.akarpov.ru/msuic">otomir23's client</a></p>
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if last_fm_account %}
|
||||
<p>Last.fm connected to {{ last_fm_account }}, <a href="{% url 'music:lastfm_connect' %}">reconnect</a></p>
|
||||
{% else %}
|
||||
<p>Last.fm is not connected, <a href="{% url 'music:lastfm_connect' %}">connect</a></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>Login to connect last.fm account</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.2.8 on 2024-01-14 12:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("users", "0013_user_agree_data_to_be_sold"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="agree_data_to_be_sold",
|
||||
field=models.BooleanField(
|
||||
default=False, verbose_name="Agree my data to be sold to vendors"
|
||||
),
|
||||
),
|
||||
]
|
20
akarpov/users/themes/migrations/0002_alter_theme_color.py
Normal file
20
akarpov/users/themes/migrations/0002_alter_theme_color.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.8 on 2024-01-14 12:06
|
||||
|
||||
import colorfield.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("themes", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="theme",
|
||||
name="color",
|
||||
field=colorfield.fields.ColorField(
|
||||
default="#FFFFFF", image_field=None, max_length=25, samples=None
|
||||
),
|
||||
),
|
||||
]
|
|
@ -599,6 +599,10 @@
|
|||
# YANDEX_MUSIC
|
||||
MUSIC_YANDEX_TOKEN = env("YANDEX_TOKEN", default="")
|
||||
|
||||
# LAST.FM
|
||||
LAST_FM_API_KEY = env("LAST_FM_API_KET", default="")
|
||||
LAST_FM_SECRET = env("LAST_FM_SECRET", default="")
|
||||
|
||||
# ROBOTS
|
||||
# ------------------------------------------------------------------------------
|
||||
ROBOTS_USE_SITEMAP = True
|
||||
|
|
90
poetry.lock
generated
90
poetry.lock
generated
|
@ -2035,13 +2035,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "drf-spectacular"
|
||||
version = "0.26.5"
|
||||
version = "0.27.0"
|
||||
description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "drf-spectacular-0.26.5.tar.gz", hash = "sha256:aee55330a774ba8a9cbdb125714d1c9ee05a8aafd3ce3be8bfd26527649aeb44"},
|
||||
{file = "drf_spectacular-0.26.5-py3-none-any.whl", hash = "sha256:c0002a820b11771fdbf37853deb371947caf0159d1afeeffe7598e964bc1db94"},
|
||||
{file = "drf-spectacular-0.27.0.tar.gz", hash = "sha256:18d7ae74b2b5d533fd31f1c591ebaa5cce1447e0976ced927401e3163040dea9"},
|
||||
{file = "drf_spectacular-0.27.0-py3-none-any.whl", hash = "sha256:6ab2d20674244e8c940c2883f744b43c34fc68c70ea3aefa802f574108c9699b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -2728,6 +2728,51 @@ files = [
|
|||
{file = "hiredis-2.3.2.tar.gz", hash = "sha256:733e2456b68f3f126ddaf2cd500a33b25146c3676b97ea843665717bda0c5d43"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.2"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
|
||||
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.23.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.26.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
|
||||
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "humanize"
|
||||
version = "4.9.0"
|
||||
|
@ -3521,6 +3566,16 @@ files = [
|
|||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
|
@ -4748,6 +4803,23 @@ check = ["check-manifest", "docutils", "flake8", "flake8-black", "flake8-depreca
|
|||
docs = ["sphinx (>=1.8)", "sphinx-intl", "sphinx-py3doc-enhanced-theme", "sphinx-rtd-theme"]
|
||||
test = ["coverage[toml] (>=5.2)", "py-cpuinfo", "pytest", "pytest-benchmark", "pytest-cov", "pytest-pep8", "pytest-profiling"]
|
||||
|
||||
[[package]]
|
||||
name = "pylast"
|
||||
version = "5.2.0"
|
||||
description = "A Python interface to Last.fm and Libre.fm"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pylast-5.2.0-py3-none-any.whl", hash = "sha256:89c7c01ea9f08c83865999d8907835157a8096e77dd9dc23420246eb66cfcff5"},
|
||||
{file = "pylast-5.2.0.tar.gz", hash = "sha256:bb046804ef56a0c18072c750d61a282d47ac102a3b0b9c44a023eaf5b0934b0a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
httpx = "*"
|
||||
|
||||
[package.extras]
|
||||
tests = ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "3.0.3"
|
||||
|
@ -5222,6 +5294,7 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
|
@ -5229,8 +5302,15 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
|
@ -5247,6 +5327,7 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
|
@ -5254,6 +5335,7 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
|
@ -7327,4 +7409,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "eb20087fe00cc1a298f95eaa4d6ca08ba69ab0ff8babb39182d8fbc21592cb29"
|
||||
content-hash = "386773a631caacada37c77c53dcce0dedf6b357bd0ec8482966cdebee587b391"
|
||||
|
|
|
@ -32,7 +32,7 @@ django-colorfield = "^0.11.0"
|
|||
djangorestframework = "^3.14.0"
|
||||
django-rest-auth = "^0.9.5"
|
||||
django-cors-headers = "^4.0.0"
|
||||
drf-spectacular = "^0.26.2"
|
||||
drf-spectacular = "^0.27.0"
|
||||
werkzeug = {extras = ["watchdog"], version = "^2.3.4"}
|
||||
ipdb = "^0.13.13"
|
||||
watchfiles = "^0.18.1"
|
||||
|
@ -119,6 +119,7 @@ qrcode = {extras = ["pil"], version = "^7.4.2"}
|
|||
spotdl = "^4.2.4"
|
||||
fuzzywuzzy = "^0.18.0"
|
||||
python-levenshtein = "^0.23.0"
|
||||
pylast = "^5.2.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue
Block a user