Compare commits

..

No commits in common. "4aa1d207aa3a6d9bb00ed1feffe35af03e708e9e" and "2b2c16db2d88989f24ddd1b479f3aeb9d5ca02f6" have entirely different histories.

13 changed files with 45 additions and 176 deletions

View File

@ -30,7 +30,6 @@
) )
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):
@ -392,27 +391,12 @@ 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=self.request.data.get("song", "")) song = Song.objects.cache().get(slug=data["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={
@ -422,11 +406,11 @@ def post(self, request, *args, **kwargs):
}, },
countdown=2, countdown=2,
) )
elif "user_id" in self.request.data: elif "user_id" in data:
listen_to_song.apply_async( listen_to_song.apply_async(
kwargs={ kwargs={
"song_id": song.id, "song_id": song.id,
"user_id": self.request.data.get("user_id", None), "user_id": data["user_id"],
"anon": True, "anon": True,
}, },
countdown=2, countdown=2,

View File

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

View File

@ -86,11 +86,6 @@ 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,11 +2,7 @@
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
@ -126,15 +122,12 @@ 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,11 +3,7 @@
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,6 +1,4 @@
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
@ -12,60 +10,33 @@ 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=[ fields=["name^5", "authors.name^3", "album.name^3"],
"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( query=ES_Q("wildcard", authors__name__raw=f"*{query.lower()}*"),
"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( query=ES_Q("wildcard", album__name__raw=f"*{query.lower()}*"),
"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).extra(size=20) search = search.query(search_query)
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(
@ -75,24 +46,3 @@ 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,40 +75,13 @@ def load_file_meta(track: int, user_id: int) -> str:
return str(song) return str(song)
def load_url(link: str, user_id: int): def load_playlist(link: str, user_id: int):
client = login()
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] author = link.split("/")[4]
playlist_id = link.split("/")[-1]
playlist = client.users_playlists(obj_id, author) # type: Playlist client = login()
playlist = client.users_playlists(int(playlist_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 * 60, time_limit=60 * 120) @shared_task(soft_time_limit=60 * 20, time_limit=60 * 30)
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_url(url, user_id) yandex.load_playlist(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,18 +211,11 @@ 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.get_first_author_name() artist_name = song.artists_names
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, artist=artist_name, title=track_name, timestamp=timestamp
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> or my <a href="https://t.me/akarpov_music_bot">inline telegram bot</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></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,8 +1,6 @@
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 User, UserAPIToken from akarpov.users.models import UserAPIToken
from akarpov.users.tasks import set_last_active_token from akarpov.users.tasks import set_last_active_token
@ -21,14 +19,4 @@ def authenticate(self, request):
return None return None
set_last_active_token.delay(token.token) set_last_active_token.delay(token.token)
return User.objects.cache().get(id=token.user_id), token return token.user, 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,8 +18,6 @@
) )
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,16 +214,23 @@ 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)
@ -235,6 +242,7 @@ 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,7 +80,6 @@
"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")
@ -529,11 +528,6 @@
{"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
@ -754,7 +748,6 @@
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