Compare commits

...

22 Commits

Author SHA1 Message Date
4aa1d207aa bug fixes 2024-04-22 15:52:33 +03:00
f5545ed7d4 bug fixes 2024-04-22 15:35:51 +03:00
1fb6219b7c bug fixes 2024-04-22 15:15:42 +03:00
b4124d90bb bug fixes 2024-04-22 14:55:07 +03:00
a6740c4faf bug fixes 2024-04-22 14:40:20 +03:00
67dc026c7e fixed search 2024-04-22 14:29:45 +03:00
7e188865cc updated music search 2024-04-22 14:17:11 +03:00
4a2b86509e updated search 2024-04-11 12:48:09 +03:00
c6063942c2 fixed translator error 2024-04-11 12:37:08 +03:00
0fa2d34e2f updated search 2024-04-11 12:29:10 +03:00
f6e2d1fe4b updated last fm scrobling 2024-04-10 17:15:41 +03:00
9dd23e1a01 updated last fm scrobling 2024-04-08 14:08:56 +03:00
6bce18344f updated caching, api docs 2024-04-07 23:28:29 +03:00
c772c1a97b updated caches 2024-04-07 23:14:33 +03:00
709bda158a bug fixes 2024-04-07 23:05:25 +03:00
e9dcccbced bug fixes 2024-04-07 22:58:29 +03:00
06afed5882 bug fixes 2024-04-07 22:51:32 +03:00
2cce8813e9 updated file serving 2024-04-07 14:59:10 +03:00
d49be5c42e bug fixes 2024-04-07 00:22:51 +03:00
64f28fc1c8 updated music info 2024-04-07 00:06:54 +03:00
b148a3d591 updated music fetching 2024-04-06 23:48:03 +03:00
00668b6f18 updated music fetching 2024-04-06 23:34:05 +03:00
13 changed files with 176 additions and 45 deletions

View File

@ -30,6 +30,7 @@
) )
from akarpov.music.services.search import search_song from akarpov.music.services.search import search_song
from akarpov.music.tasks import listen_to_song from akarpov.music.tasks import listen_to_song
from akarpov.users.models import User
class LikedSongsContextMixin(generics.GenericAPIView): class LikedSongsContextMixin(generics.GenericAPIView):
@ -391,12 +392,27 @@ def get_queryset(self):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=False) serializer.is_valid(raise_exception=False)
data = serializer.validated_data
try: try:
song = Song.objects.cache().get(slug=data["song"]) song = Song.objects.cache().get(slug=self.request.data.get("song", ""))
except Song.DoesNotExist: except Song.DoesNotExist:
return Response(status=404) return Response(status=404)
try:
user_id = self.request.data.get("user_id", None)
if user_id:
user_id_int = None
try:
user_id_int = int(user_id)
except ValueError:
...
if user_id_int:
user = User.objects.cache().get(id=user_id_int)
if user != self.request.user:
return Response(status=403)
except User.DoesNotExist:
...
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
listen_to_song.apply_async( listen_to_song.apply_async(
kwargs={ kwargs={
@ -406,11 +422,11 @@ def post(self, request, *args, **kwargs):
}, },
countdown=2, countdown=2,
) )
elif "user_id" in data: elif "user_id" in self.request.data:
listen_to_song.apply_async( listen_to_song.apply_async(
kwargs={ kwargs={
"song_id": song.id, "song_id": song.id,
"user_id": data["user_id"], "user_id": self.request.data.get("user_id", None),
"anon": True, "anon": True,
}, },
countdown=2, countdown=2,

View File

@ -35,9 +35,11 @@ class SongDocument(Document):
name = fields.TextField( name = fields.TextField(
attr="name", attr="name",
fields={ fields={
"raw": fields.KeywordField(normalizer="lowercase"), "raw": fields.KeywordField(),
"exact": fields.KeywordField(normalizer="lowercase"),
}, },
) )
suggest = fields.CompletionField()
meta = fields.ObjectField(dynamic=True) meta = fields.ObjectField(dynamic=True)

View File

@ -86,6 +86,11 @@ def album_name(self):
def artists_names(self): def artists_names(self):
return cache_model_property(self, "_authors_names") return cache_model_property(self, "_authors_names")
def get_first_author_name(self):
if self.authors:
return self.authors.first().name
return ""
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -2,7 +2,11 @@
import re import re
import requests import requests
from deep_translator import GoogleTranslator
try:
from deep_translator import GoogleTranslator # TODO: move to another service
except requests.exceptions.JSONDecodeError:
print("Failed to initialize GoogleTranslator due to external API issues.")
from django.core.files import File from django.core.files import File
from django.db import transaction from django.db import transaction
from django.utils.text import slugify from django.utils.text import slugify
@ -122,12 +126,15 @@ def load_track(
album=album, album=album,
): ):
return sng.first() return sng.first()
try:
if not path.endswith(".mp3"): if not path.endswith(".mp3"):
mp3_path = path.replace(path.split(".")[-1], "mp3") mp3_path = path.replace(path.split(".")[-1], "mp3")
AudioSegment.from_file(path).export(mp3_path) AudioSegment.from_file(path).export(mp3_path)
os.remove(path) os.remove(path)
path = mp3_path path = mp3_path
except Exception as e:
print(e)
return Song.objects.none()
tag = MP3(path, ID3=ID3) tag = MP3(path, ID3=ID3)

View File

@ -3,7 +3,11 @@
import requests import requests
import spotipy import spotipy
try:
from deep_translator import GoogleTranslator from deep_translator import GoogleTranslator
except requests.exceptions.JSONDecodeError:
print("Failed to initialize GoogleTranslator due to external API issues.")
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
from django.db import transaction from django.db import transaction

View File

@ -1,4 +1,6 @@
from django.core.cache import cache
from django.db.models import Case, When from django.db.models import Case, When
from django_elasticsearch_dsl.registries import registry
from elasticsearch_dsl import Q as ES_Q from elasticsearch_dsl import Q as ES_Q
from akarpov.music.documents import SongDocument from akarpov.music.documents import SongDocument
@ -10,33 +12,60 @@ def search_song(query):
search_query = ES_Q( search_query = ES_Q(
"bool", "bool",
should=[ should=[
ES_Q("match", name=query),
ES_Q("match", name__russian=query),
ES_Q( ES_Q(
"multi_match", "multi_match",
query=query, query=query,
fields=["name^5", "authors.name^3", "album.name^3"], fields=[
"name^5",
"name.russian^5",
"authors.name^3",
"authors.name.raw^3",
"album.name^3",
"album.name.raw^3",
"name.raw^2",
],
type="best_fields",
fuzziness="AUTO", fuzziness="AUTO",
), ),
ES_Q("wildcard", name__raw=f"*{query.lower()}*"),
ES_Q( ES_Q(
"nested", "nested",
path="authors", path="authors",
query=ES_Q("wildcard", authors__name__raw=f"*{query.lower()}*"), query=ES_Q(
"multi_match",
query=query,
fields=["authors.name", "authors.name.raw"],
fuzziness="AUTO",
), ),
),
# Correcting wildcard queries with the proper syntax:
ES_Q("wildcard", **{"name.raw": f"*{query.lower()}*"}),
ES_Q( ES_Q(
"nested", "nested",
path="album", path="album",
query=ES_Q("wildcard", album__name__raw=f"*{query.lower()}*"), query=ES_Q(
"multi_match",
query=query,
fields=["album.name", "album.name.raw"],
fuzziness="AUTO",
), ),
ES_Q("wildcard", meta__raw=f"*{query.lower()}*"), ),
# Ensuring the nested wildcard query is properly structured
ES_Q(
"nested",
path="album",
query=ES_Q("wildcard", **{"album.name.raw": f"*{query.lower()}*"}),
),
# Correcting the wildcard query for `meta.raw`
ES_Q("wildcard", **{"meta.raw": f"*{query.lower()}*"}),
], ],
minimum_should_match=1, minimum_should_match=1,
) )
search = search.query(search_query) search = search.query(search_query).extra(size=20)
response = search.execute() response = search.execute()
# Check for hits and get song instances
if response.hits: if response.hits:
hit_ids = [hit.meta.id for hit in response.hits] hit_ids = [hit.meta.id for hit in response.hits]
songs = Song.objects.filter(id__in=hit_ids).order_by( songs = Song.objects.filter(id__in=hit_ids).order_by(
@ -46,3 +75,24 @@ def search_song(query):
return songs return songs
return Song.objects.none() return Song.objects.none()
def autocomplete_search(query):
s = SongDocument.search()
s = s.suggest("song_suggest", query, completion={"field": "suggest"})
suggestions = s.execute().suggest.song_suggest[0].options
return [option.text for option in suggestions]
def get_popular_songs():
if "popular_songs" in cache:
return cache.get("popular_songs")
else:
songs = Song.objects.filter(played__gt=300).order_by("-played")[:10]
cache.set("popular_songs", songs, timeout=3600)
return songs
def bulk_update_index(model_class):
qs = model_class.objects.all()
registry.update(qs, bulk_size=100)

View File

@ -75,13 +75,40 @@ def load_file_meta(track: int, user_id: int) -> str:
return str(song) return str(song)
def load_playlist(link: str, user_id: int): def load_url(link: str, user_id: int):
author = link.split("/")[4]
playlist_id = link.split("/")[-1]
client = login() client = login()
playlist = client.users_playlists(int(playlist_id), author) # type: Playlist obj_id = link.split("/")[-1]
obj_id = obj_id.split("?")[0]
try:
obj_id = int(obj_id)
except ValueError:
print("Invalid link")
return None
if "/playlists/" in link:
author = link.split("/")[4]
playlist = client.users_playlists(obj_id, author) # type: Playlist
for track in playlist.fetch_tracks(): for track in playlist.fetch_tracks():
tasks.load_ym_file_meta.apply_async( tasks.load_ym_file_meta.apply_async(
kwargs={"track": track.track.id, "user_id": user_id} kwargs={"track": track.track.id, "user_id": user_id}
) )
elif "/album/" in link:
album = client.albums_with_tracks(obj_id)
for volume in album.volumes:
for track in volume:
tasks.load_ym_file_meta.apply_async(
kwargs={"track": track.id, "user_id": user_id}
)
elif "/artist/" in link:
artist = client.artists(obj_id)[0]
albums = artist.get_albums(page_size=100)
for album in albums:
for track in album.fetch_tracks():
tasks.load_ym_file_meta.apply_async(
kwargs={"track": track.id, "user_id": user_id}
)
else:
tasks.load_ym_file_meta.apply_async(
kwargs={"track": obj_id, "user_id": user_id}
)

View File

@ -29,7 +29,7 @@
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@shared_task(soft_time_limit=60 * 20, time_limit=60 * 30) @shared_task(soft_time_limit=60 * 60, time_limit=60 * 120)
def list_tracks(url, user_id): def list_tracks(url, user_id):
if "music.youtube.com" in url or "youtu.be" in url: if "music.youtube.com" in url or "youtu.be" in url:
url = url.replace("music.youtube.com", "youtube.com") url = url.replace("music.youtube.com", "youtube.com")
@ -37,7 +37,7 @@ def list_tracks(url, user_id):
if "spotify.com" in url: if "spotify.com" in url:
spotify.download_url(url, user_id) spotify.download_url(url, user_id)
elif "music.yandex.ru" in url: elif "music.yandex.ru" in url:
yandex.load_playlist(url, user_id) yandex.load_url(url, user_id)
if "youtube.com" in url: if "youtube.com" in url:
if "channel" in url or "/c/" in url: if "channel" in url or "/c/" in url:
ytmusic = ytmusicapi.YTMusic() ytmusic = ytmusicapi.YTMusic()
@ -211,11 +211,18 @@ def listen_to_song(song_id, user_id=None, anon=True):
session_key=lastfm_token, session_key=lastfm_token,
) )
song = Song.objects.get(id=song_id) song = Song.objects.get(id=song_id)
artist_name = song.artists_names artist_name = song.get_first_author_name()
track_name = song.name track_name = song.name
album_name = song.album.name
timestamp = int(timezone.now().timestamp()) timestamp = int(timezone.now().timestamp())
network.scrobble( network.scrobble(
artist=artist_name, title=track_name, timestamp=timestamp artist=artist_name,
title=track_name,
timestamp=timestamp,
album=album_name,
)
network.update_now_playing(
artist=artist_name, title=track_name, album=album_name
) )
except UserMusicProfile.DoesNotExist: except UserMusicProfile.DoesNotExist:
pass pass

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<h1>Welcome to music app</h1> <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/music">otomir23's client</a></p> <p>This is mainly the backend of music, you should consider using side clients like: <a href="https://next.akarpov.ru/music">otomir23's client</a> or my <a href="https://t.me/akarpov_music_bot">inline telegram bot</a></p>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% if last_fm_account %} {% if last_fm_account %}
<p>Last.fm connected to {{ last_fm_account }}, <a href="{% url 'music:lastfm_connect' %}">reconnect</a></p> <p>Last.fm connected to {{ last_fm_account }}, <a href="{% url 'music:lastfm_connect' %}">reconnect</a></p>

View File

@ -1,6 +1,8 @@
from drf_spectacular.extensions import OpenApiAuthenticationExtension
from drf_spectacular.plumbing import build_bearer_security_scheme_object
from rest_framework.authentication import BaseAuthentication from rest_framework.authentication import BaseAuthentication
from akarpov.users.models import UserAPIToken from akarpov.users.models import User, UserAPIToken
from akarpov.users.tasks import set_last_active_token from akarpov.users.tasks import set_last_active_token
@ -19,4 +21,14 @@ def authenticate(self, request):
return None return None
set_last_active_token.delay(token.token) set_last_active_token.delay(token.token)
return token.user, token return User.objects.cache().get(id=token.user_id), token
class UserTokenAuthenticationExtension(OpenApiAuthenticationExtension):
target_class = "akarpov.users.api.authentification.UserTokenAuthentication"
name = "UserTokenAuthentication"
def get_security_definition(self, auto_schema):
return build_bearer_security_scheme_object(
header_name="Authorization", token_prefix="Bearer"
)

View File

@ -18,6 +18,8 @@
) )
from akarpov.users.models import User from akarpov.users.models import User
from .authentification import UserTokenAuthentication # noqa: F401
class UserRegisterAPIViewSet(generics.CreateAPIView): class UserRegisterAPIViewSet(generics.CreateAPIView):
"""Creates new user and sends verification email""" """Creates new user and sends verification email"""

View File

@ -214,23 +214,16 @@ def list_tokens(request):
@login_required @login_required
def create_token(request): def create_token(request):
initial_data = {} initial_data = {}
# Обработка параметров 'name' и 'active_until'
if "name" in request.GET: if "name" in request.GET:
initial_data["name"] = request.GET["name"] initial_data["name"] = request.GET["name"]
if "active_until" in request.GET: if "active_until" in request.GET:
initial_data["active_until"] = request.GET["active_until"] initial_data["active_until"] = request.GET["active_until"]
# Создаем QueryDict для разрешений, чтобы правильно обработать повторяющиеся ключи
permissions_query_dict = QueryDict("", mutable=True) permissions_query_dict = QueryDict("", mutable=True)
# Разбор параметров разрешений
permissions = request.GET.getlist("permissions") permissions = request.GET.getlist("permissions")
for perm in permissions: for perm in permissions:
category, permission = perm.split(".") category, permission = perm.split(".")
permissions_query_dict.update({f"permissions_{category}": [permission]}) permissions_query_dict.update({f"permissions_{category}": [permission]})
# Переводим QueryDict в обычный словарь для использования в initial
permissions_data = {key: value for key, value in permissions_query_dict.lists()} permissions_data = {key: value for key, value in permissions_query_dict.lists()}
initial_data.update(permissions_data) initial_data.update(permissions_data)
@ -242,7 +235,6 @@ def create_token(request):
initial=initial_data, permissions_context=UserAPIToken.permission_template initial=initial_data, permissions_context=UserAPIToken.permission_template
) )
if request.method == "POST": if request.method == "POST":
print(request.POST)
form = TokenCreationForm(request.POST) form = TokenCreationForm(request.POST)
if form.is_valid(): if form.is_valid():
new_token = form.save(commit=False) new_token = form.save(commit=False)

View File

@ -80,6 +80,7 @@
"music.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15}, "music.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
"otp_totp.totpdevice": {"ops": "all", "timeout": 15 * 60}, "otp_totp.totpdevice": {"ops": "all", "timeout": 15 * 60},
"users.userapitoken": {"ops": "all", "timeout": 20 * 60}, "users.userapitoken": {"ops": "all", "timeout": 20 * 60},
"users.user": {"ops": "all", "timeout": 5 * 60},
} }
CACHEOPS_REDIS = env.str("REDIS_URL") CACHEOPS_REDIS = env.str("REDIS_URL")
@ -528,6 +529,11 @@
{"url": "http://127.0.0.1:8000", "description": "Local Development server"}, {"url": "http://127.0.0.1:8000", "description": "Local Development server"},
{"url": "https://new.akarpov.ru", "description": "Production server"}, {"url": "https://new.akarpov.ru", "description": "Production server"},
], ],
"EXTENSIONS": {
"authentication": [
"akarpov.users.api.authentification.UserTokenAuthenticationExtension"
],
},
} }
# CKEDITOR # CKEDITOR
@ -748,6 +754,7 @@
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
"default": {"hosts": env("ELASTIC_SEARCH", default="http://127.0.0.1:9200/")}, "default": {"hosts": env("ELASTIC_SEARCH", default="http://127.0.0.1:9200/")},
} }
USE_DEBUG_TOOLBAR = False USE_DEBUG_TOOLBAR = False
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True