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.tasks import listen_to_song
from akarpov.users.models import User
class LikedSongsContextMixin(generics.GenericAPIView):
@ -391,12 +392,27 @@ def get_queryset(self):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=False)
data = serializer.validated_data
try:
song = Song.objects.cache().get(slug=data["song"])
song = Song.objects.cache().get(slug=self.request.data.get("song", ""))
except Song.DoesNotExist:
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:
listen_to_song.apply_async(
kwargs={
@ -406,11 +422,11 @@ def post(self, request, *args, **kwargs):
},
countdown=2,
)
elif "user_id" in data:
elif "user_id" in self.request.data:
listen_to_song.apply_async(
kwargs={
"song_id": song.id,
"user_id": data["user_id"],
"user_id": self.request.data.get("user_id", None),
"anon": True,
},
countdown=2,

View File

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

View File

@ -86,6 +86,11 @@ def album_name(self):
def artists_names(self):
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):
return self.name

View File

@ -2,7 +2,11 @@
import re
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.db import transaction
from django.utils.text import slugify
@ -122,12 +126,15 @@ def load_track(
album=album,
):
return sng.first()
try:
if not path.endswith(".mp3"):
mp3_path = path.replace(path.split(".")[-1], "mp3")
AudioSegment.from_file(path).export(mp3_path)
os.remove(path)
path = mp3_path
except Exception as e:
print(e)
return Song.objects.none()
tag = MP3(path, ID3=ID3)

View File

@ -3,7 +3,11 @@
import requests
import spotipy
try:
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.core.files import File
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_elasticsearch_dsl.registries import registry
from elasticsearch_dsl import Q as ES_Q
from akarpov.music.documents import SongDocument
@ -10,33 +12,60 @@ def search_song(query):
search_query = ES_Q(
"bool",
should=[
ES_Q("match", name=query),
ES_Q("match", name__russian=query),
ES_Q(
"multi_match",
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",
),
ES_Q("wildcard", name__raw=f"*{query.lower()}*"),
ES_Q(
"nested",
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(
"nested",
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,
)
search = search.query(search_query)
search = search.query(search_query).extra(size=20)
response = search.execute()
# Check for hits and get song instances
if response.hits:
hit_ids = [hit.meta.id for hit in response.hits]
songs = Song.objects.filter(id__in=hit_ids).order_by(
@ -46,3 +75,24 @@ def search_song(query):
return songs
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)
def load_playlist(link: str, user_id: int):
author = link.split("/")[4]
playlist_id = link.split("/")[-1]
def load_url(link: str, user_id: int):
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():
tasks.load_ym_file_meta.apply_async(
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__)
@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):
if "music.youtube.com" in url or "youtu.be" in url:
url = url.replace("music.youtube.com", "youtube.com")
@ -37,7 +37,7 @@ def list_tracks(url, user_id):
if "spotify.com" in url:
spotify.download_url(url, user_id)
elif "music.yandex.ru" in url:
yandex.load_playlist(url, user_id)
yandex.load_url(url, user_id)
if "youtube.com" in url:
if "channel" in url or "/c/" in url:
ytmusic = ytmusicapi.YTMusic()
@ -211,11 +211,18 @@ def listen_to_song(song_id, user_id=None, anon=True):
session_key=lastfm_token,
)
song = Song.objects.get(id=song_id)
artist_name = song.artists_names
artist_name = song.get_first_author_name()
track_name = song.name
album_name = song.album.name
timestamp = int(timezone.now().timestamp())
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:
pass

View File

@ -2,7 +2,7 @@
{% 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/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 last_fm_account %}
<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 akarpov.users.models import UserAPIToken
from akarpov.users.models import User, UserAPIToken
from akarpov.users.tasks import set_last_active_token
@ -19,4 +21,14 @@ def authenticate(self, request):
return None
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 .authentification import UserTokenAuthentication # noqa: F401
class UserRegisterAPIViewSet(generics.CreateAPIView):
"""Creates new user and sends verification email"""

View File

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

View File

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