Изменил метод получения ссылки на аудио, чтобы можно было логгировать прослушивание #8

Merged
lovinervy merged 1 commits from dev/music-log into main 2026-01-07 12:50:13 +05:00
8 changed files with 103 additions and 19 deletions

View File

@@ -8,6 +8,7 @@ from django.utils.html import format_html
from music.models import Track from music.models import Track
from music.models import Album from music.models import Album
from music.models import Artist from music.models import Artist
from music.models import MusicLog
from music.models import Playlist from music.models import Playlist
from music.models import RecommendationPlaylist from music.models import RecommendationPlaylist
@@ -105,3 +106,11 @@ class RecommendationPlaylistAdmin(admin.ModelAdmin):
def make_actual(self, request: HttpRequest, queryset: Any) -> None: def make_actual(self, request: HttpRequest, queryset: Any) -> None:
for recommendation in queryset: for recommendation in queryset:
recommendation.switch_actual() recommendation.switch_actual()
@admin.register(MusicLog)
class MusicLogAdmin(admin.ModelAdmin):
list_display = ("track", "played_at")
search_fields = ("track__title", "track__album__artist__name", "track__album__name")
list_filter = ("played_at",)
readonly_fields = ("track", "played_at")

View File

@@ -0,0 +1,10 @@
from django.urls import path
from music.views import TrackAPIView
app_name = "music_api"
urlpatterns = [
path("tracks/<int:pk>/", TrackAPIView.as_view(), name="track_detail"),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 6.0 on 2026-01-07 07:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0012_alter_recommendationplaylist_playlist'),
]
operations = [
migrations.CreateModel(
name='MusicLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('played_at', models.DateTimeField(auto_now_add=True)),
('user_ip', models.GenericIPAddressField(blank=True, null=True)),
('track', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='music.track')),
],
),
]

View File

@@ -116,3 +116,12 @@ class RecommendationPlaylist(BaseModel):
.update(is_actual=False)) .update(is_actual=False))
self.is_actual = True self.is_actual = True
self.save() self.save()
class MusicLog(models.Model):
track = models.ForeignKey(Track, on_delete=models.CASCADE, related_name="logs")
played_at = models.DateTimeField(auto_now_add=True)
user_ip = models.GenericIPAddressField(null=True, blank=True)
def __str__(self):
return f"Played {self.track} at {self.played_at}"

View File

@@ -1,12 +1,14 @@
from django import views as django_views from django import views as django_views
from django.views.generic import ListView from django.views.generic import ListView
from django.shortcuts import render from django.http import JsonResponse
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.shortcuts import render
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from music.models import Track from music.models import Track
from music.models import Artist from music.models import Artist
from music.models import Album from music.models import Album
from music.models import MusicLog
class TrackListView(ListView): class TrackListView(ListView):
@@ -61,3 +63,24 @@ class AlbumDetailView(django_views.View):
def get(self, request: HttpRequest, pk: int, *args, **kwargs): def get(self, request: HttpRequest, pk: int, *args, **kwargs):
album = get_object_or_404(Album, id=pk) album = get_object_or_404(Album, id=pk)
return render(request, "music/album_detail.html", {"album": album}) return render(request, "music/album_detail.html", {"album": album})
class TrackAPIView(django_views.View):
def get(self, request: HttpRequest, pk: int, *args, **kwargs):
track = get_object_or_404(Track, id=pk)
data = {
"id": track.id,
"title": track.title,
"url": track.file.url,
"album": {
"id": track.album.id,
"title": track.album.name,
"cover_image": track.album.preview_image.url,
"artist": {
"id": track.album.artist.id,
"name": track.album.artist.name,
},
},
}
MusicLog.objects.create(track=track, user_ip=request.META.get("REMOTE_ADDR", ""))
return JsonResponse(data)

View File

@@ -21,4 +21,6 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('music/', include("music.urls")), path('music/', include("music.urls")),
path('', include("core.urls")), path('', include("core.urls")),
path('api/music/', include("music.api")),
] ]

View File

@@ -53,12 +53,13 @@ document.addEventListener('DOMContentLoaded', function () {
updateProgressUI(0, audioPlayer.duration); updateProgressUI(0, audioPlayer.duration);
}); });
function updateMediaSession(title, artist) { function updateMediaSession(title, artist, coverImage) {
if (!('mediaSession' in navigator)) return; if (!('mediaSession' in navigator)) return;
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: title, title: title,
artist: artist artist: artist,
artwork: [coverImage ? { src: coverImage, sizes: '512x512', type: 'image/png' } : null].filter(Boolean),
}); });
navigator.mediaSession.setActionHandler('play', () => audioPlayer.play()); navigator.mediaSession.setActionHandler('play', () => audioPlayer.play());
@@ -81,25 +82,32 @@ document.addEventListener('DOMContentLoaded', function () {
currentIndex = index; currentIndex = index;
const item = trackItems[index]; const item = trackItems[index];
const src = item.dataset.trackSrc; const api = item.dataset.trackSrc;
const title = item.querySelector('.track-title').textContent; fetch(api)
const artist = item.querySelector('.track-artist').textContent.split(': ')[1]; .then(response => response.json())
.then(data => {
const src = data.url;
const title = data.title;
const artist = data.album.artist.name;
const coverImage = data.album.cover_image;
resetActiveTrack(); resetActiveTrack();
item.classList.add('active'); item.classList.add('active');
updateUI(title, artist); updateUI(title, artist, coverImage);
// --------------------------- // ---------------------------
// ВЫЗОВ Media Session API // ВЫЗОВ Media Session API
// --------------------------- updateMediaSession(title, artist, coverImage);
updateMediaSession(title, artist); // ---------------------------
// ---------------------------
audioPlayer.src = src; audioPlayer.src = src;
audioPlayer.title = artist + ' - ' + title; audioPlayer.title = artist + ' - ' + title;
audioPlayer.play().catch(err => console.log(err)); audioPlayer.play().catch(err => console.log(err));
updatePlayButton(true); updatePlayButton(true);
})
.catch(err => console.log(err));
} }
// Клик по треку // Клик по треку

View File

@@ -5,7 +5,7 @@
<ul class="track-list"> <ul class="track-list">
{% if tracks %} {% if tracks %}
{% for track in tracks %} {% for track in tracks %}
<li class="track-item" data-track-id="{{ track.id }}" data-track-src="{{ track.file.url }}"> <li class="track-item" data-track-id="{{ track.id }}" data-track-src="{% url 'music_api:track_detail' track.id %}">
<div class="track-info"> <div class="track-info">
<h3 class="track-title">{{ track.title }}</h3> <h3 class="track-title">{{ track.title }}</h3>
<p class="track-artist">Исполнитель: {{ track.album.artist }}</p> <p class="track-artist">Исполнитель: {{ track.album.artist }}</p>