mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-10 23:06:32 +03:00
Compare commits
22 Commits
2b2c16db2d
...
4aa1d207aa
Author | SHA1 | Date | |
---|---|---|---|
4aa1d207aa | |||
f5545ed7d4 | |||
1fb6219b7c | |||
b4124d90bb | |||
a6740c4faf | |||
67dc026c7e | |||
7e188865cc | |||
4a2b86509e | |||
c6063942c2 | |||
0fa2d34e2f | |||
f6e2d1fe4b | |||
9dd23e1a01 | |||
6bce18344f | |||
c772c1a97b | |||
709bda158a | |||
e9dcccbced | |||
06afed5882 | |||
2cce8813e9 | |||
d49be5c42e | |||
64f28fc1c8 | |||
b148a3d591 | |||
00668b6f18 |
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user