added last fm auth and scrobling

This commit is contained in:
Alexander Karpov 2024-01-14 17:26:43 +03:00
parent cb7bbbf49f
commit dba8bc9ba4
17 changed files with 425 additions and 4 deletions

View File

@ -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
View 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>

View 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

View 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,
),
),
]

View File

@ -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)

View 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,
),
),
],
),
]

View File

@ -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"

View File

@ -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

View File

@ -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"),
]

View File

@ -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()

View File

@ -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">

View 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 %}

View File

@ -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"
),
),
]

View 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
),
),
]

View File

@ -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

64
poetry.lock generated
View File

@ -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"
@ -4748,6 +4793,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"
@ -7327,4 +7389,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 = "c42bc63ed7bf613a8430fe48f0ed6a2ae218fc995618bea29584db33ce8cc46c"

View File

@ -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]