updated gallery and music radio

This commit is contained in:
Alexander Karpov 2023-11-18 14:59:16 +03:00
parent 4ef6021499
commit e94f90d091
27 changed files with 507 additions and 363 deletions

View File

@ -2,7 +2,7 @@
from rest_framework import serializers
from akarpov.blog.models import Comment, Post, Tag
from akarpov.common.api import RecursiveField
from akarpov.common.api.serializers import RecursiveField
from akarpov.users.api.serializers import UserPublicInfoSerializer

View File

@ -9,7 +9,7 @@
)
from akarpov.blog.models import Post
from akarpov.blog.services import get_main_rating_posts
from akarpov.common.api import StandardResultsSetPagination
from akarpov.common.api.pagination import StandardResultsSetPagination
class ListMainPostsView(generics.ListAPIView):

View File

@ -1,45 +0,0 @@
from rest_framework import serializers
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import SAFE_METHODS, BasePermission
from akarpov.utils.models import get_object_user
class SmallResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100
class StandardResultsSetPagination(PageNumberPagination):
page_size = 50
page_size_query_param = "page_size"
max_page_size = 200
class BigResultsSetPagination(PageNumberPagination):
page_size = 100
page_size_query_param = "page_size"
max_page_size = 1000
class RecursiveField(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
class IsCreatorOrReadOnly(BasePermission):
def has_permission(self, request, view):
return bool(
request.method in SAFE_METHODS
or request.user
and get_object_user(view.get_object()) == request.user
)
class SetUserModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
creator = self.context["request"].user
obj = self.Meta.model.objects.create(creator=creator, **validated_data)
return obj

View File

View File

@ -0,0 +1,19 @@
from rest_framework.pagination import PageNumberPagination
class SmallResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100
class StandardResultsSetPagination(PageNumberPagination):
page_size = 50
page_size_query_param = "page_size"
max_page_size = 200
class BigResultsSetPagination(PageNumberPagination):
page_size = 100
page_size_query_param = "page_size"
max_page_size = 1000

View File

@ -0,0 +1,12 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from akarpov.utils.models import get_object_user
class IsCreatorOrReadOnly(BasePermission):
def has_permission(self, request, view):
return bool(
request.method in SAFE_METHODS
or request.user
and get_object_user(view.get_object()) == request.user
)

View File

@ -0,0 +1,14 @@
from rest_framework import serializers
class RecursiveField(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
class SetUserModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
creator = self.context["request"].user
obj = self.Meta.model.objects.create(creator=creator, **validated_data)
return obj

View File

View File

@ -0,0 +1,24 @@
from rest_framework import serializers
from akarpov.gallery.models import Image
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = (
"slug",
"image",
"collection",
"public",
"image_cropped",
"created",
"modified",
)
extra_kwargs = {
"slug": {"read_only": True},
"collection": {"write_only": True},
"image_cropped": {"read_only": True},
"created": {"read_only": True},
"modified": {"read_only": True},
}

View File

@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = "gallery"
urlpatterns = [
path("", views.ListCreateImageAPIView.as_view(), name="list-create-all"),
]

View File

@ -0,0 +1,21 @@
from rest_framework import generics, parsers
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from akarpov.common.api.pagination import StandardResultsSetPagination
from akarpov.gallery.api.serializers import ImageSerializer
from akarpov.gallery.models import Image
class ListCreateImageAPIView(generics.ListCreateAPIView):
serializer_class = ImageSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
pagination_class = StandardResultsSetPagination
def get_queryset(self):
if self.request.user.is_authenticated:
return self.request.user.images.all()
return Image.objects.filter(public=True)
def perform_create(self, serializer):
serializer.save(user=self.request.user)

40
akarpov/gallery/forms.py Normal file
View File

@ -0,0 +1,40 @@
from django import forms
from akarpov.common.forms import MultipleFileField
from akarpov.gallery.models import Collection, Image
class ImageUploadForm(forms.ModelForm):
image = MultipleFileField
class Meta:
model = Image
fields = (
"image",
"collection",
"public",
)
def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if user is not None:
self.fields["collection"].queryset = Collection.objects.filter(user=user)
def save(self, commit=True):
files = self.files.getlist("image")
instances = []
for file in files:
instance = self.instance
instance.image = file
if commit:
instance.save()
instances.append(instance)
return instances
ImageFormSet = forms.modelformset_factory(
Image,
form=ImageUploadForm,
extra=3, # Number of images to upload at once; adjust as needed
)

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.6 on 2023-11-17 20:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gallery", "0003_alter_collection_options_alter_image_options"),
]
operations = [
migrations.AddField(
model_name="image",
name="public",
field=models.BooleanField(default=False),
),
]

View File

@ -31,6 +31,7 @@ class Image(TimeStampedModel, ShortLinkModel, BaseImageModel, UserHistoryModel):
collection = models.ForeignKey(
"Collection", related_name="images", on_delete=models.CASCADE
)
public = models.BooleanField(default=False)
user = models.ForeignKey(
"users.User", related_name="images", on_delete=models.CASCADE
)

View File

@ -2,6 +2,7 @@
from akarpov.gallery.views import (
collection_view,
image_upload_view,
image_view,
list_collections_view,
list_tag_images_view,
@ -10,6 +11,7 @@
app_name = "gallery"
urlpatterns = [
path("", list_collections_view, name="list"),
path("upload/", image_upload_view, name="upload"),
path("<str:slug>", collection_view, name="collection"),
path("tag/<str:slug>", list_tag_images_view, name="tag"),
path("image/<str:slug>", image_view, name="view"),

View File

@ -1,7 +1,9 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.views import generic
from akarpov.common.views import HasPermissions
from akarpov.gallery.forms import ImageUploadForm
from akarpov.gallery.models import Collection, Image, Tag
@ -46,3 +48,25 @@ class ImageView(generic.DetailView, HasPermissions):
image_view = ImageView.as_view()
class ImageUploadView(LoginRequiredMixin, generic.CreateView):
model = Image
form_class = ImageUploadForm
success_url = "" # Replace with your success URL
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
form.save()
return self.form_valid(form)
else:
return self.form_invalid(form)
image_upload_view = ImageUploadView.as_view()

View File

@ -1,7 +1,7 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from akarpov.common.api import SetUserModelSerializer
from akarpov.common.api.serializers import SetUserModelSerializer
from akarpov.music.models import Album, Author, Playlist, Song
from akarpov.users.api.serializers import UserPublicInfoSerializer

View File

@ -1,6 +1,6 @@
from rest_framework import generics, permissions
from akarpov.common.api import IsCreatorOrReadOnly
from akarpov.common.api.permissions import IsCreatorOrReadOnly
from akarpov.music.api.serializers import (
FullPlaylistSerializer,
ListSongSerializer,

View File

@ -1,6 +1,6 @@
from rest_framework import generics, permissions
from akarpov.common.api import StandardResultsSetPagination
from akarpov.common.api.pagination import StandardResultsSetPagination
from akarpov.notifications.models import Notification
from akarpov.notifications.providers.site.api.serializers import (
SiteNotificationSerializer,

View File

@ -0,0 +1,177 @@
// Determine the correct WebSocket protocol to use
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host; // Assuming host includes the port if needed
const socketPath = `${protocol}//${host}/ws/radio/`;
let socket = new WebSocket(socketPath);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Your existing timeSince function can remain unchanged
// Update the onmessage handler here as needed for the music player
socket.onmessage = function(event) {
let data = JSON.parse(event.data);
// Process the data
};
// This function will attempt to reconnect the WebSocket after a 5-second delay
async function reconnectSocket() {
console.log("Radio socket disconnected, reconnecting...");
let socketClosed = true;
await sleep(5000); // Wait 5 seconds before attempting to reconnect
while (socketClosed) {
try {
socket = new WebSocket(socketPath);
socket.onmessage = onMessageHandler; // Reassign your onmessage handler
socket.onclose = onSocketCloseHandler; // Reassign the onclose handler to this function
socketClosed = false; // Exit the loop if the connection is successful
} catch (e) {
console.log("Can't connect to socket, retrying in 1 second...");
await sleep(1000); // Wait 1 second before retrying
}
}
}
// Define a separate function for the onclose event
const onSocketCloseHandler = async function(event) {
if (!event.wasClean) {
// Only attempt to reconnect if the socket did not close cleanly
await reconnectSocket();
}
};
// Assign the onclose handler to the socket
socket.onclose = onSocketCloseHandler;
let isAdmin = false; // This will be set based on socket connect data for admin TODO: retrieve from server
function displaySong(data) {
// Display song details using jQuery to manipulate the DOM
$('#song-info').html(/* HTML structure for song info */);
// Update admin controls if isAdmin is true
if (isAdmin) {
$('#admin-controls').show();
// Add slider and other controls
}
// Add song to history
addToHistory(data);
}
function displayAdminControls(data) {
// Display admin controls if user is admin
if (isAdmin) {
$('#admin-controls').html(/* HTML structure for admin controls */);
}
}
function updateSong(data) {
// Update the song information on the page
displaySong(data);
}
function addToHistory(songData) {
var authors = songData.authors.map(author => author.name).join(", ");
var historyItemHtml = `
<div class="card mb-3" style="max-width: 540px;">
<div class="row g-0">
<div class="col-md-4">
<img src="${songData.image}" class="img-fluid rounded-start" alt="${songData.name}">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${songData.name}</h5>
<p class="card-text"><small class="text-muted">by ${authors}</small></p>
<p class="card-text">${songData.album.name}</p>
<!-- Admin controls would go here if needed -->
</div>
</div>
</div>
</div>
`;
$('#history').prepend(historyItemHtml); // prepend to add it to the top of the list
}
// Functionality to handle sliding track length and selecting track to play
// ...
// ws_script.js
$(document).ready(function() {
var audioContext;
var analyser;
var started = false; // Flag to indicate if the audio context has started
var audio = document.getElementById('audio-player');
var canvas = document.getElementById('audio-visualization');
var ctx = canvas.getContext('2d');
// Function to initialize the AudioContext and analyser
function initAudioContext() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
var audioSrc = audioContext.createMediaElementSource(audio);
audioSrc.connect(analyser);
analyser.connect(audioContext.destination);
}
// Function to start or resume the audio context on user interaction
function startOrResumeContext() {
if (audioContext.state === 'suspended') {
audioContext.resume().then(() => {
console.log('Playback resumed successfully');
});
}
}
// Volume control listener
$('#volume-control').on('input change', function() {
if (audio) {
audio.volume = $(this).val() / 100;
}
});
// Listener for play/pause button
$('#play-pause-button').on('click', function() {
if (!started) {
initAudioContext();
started = true;
}
startOrResumeContext(); // Resume the audio context if needed
if (audio.paused) {
audio.play().then(() => {
console.log('Audio playing');
}).catch((e) => {
console.error('Error playing audio:', e);
});
} else {
audio.pause();
console.log('Audio paused');
}
});
function renderFrame() {
if (!analyser) return;
requestAnimationFrame(renderFrame);
var fbc_array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(fbc_array);
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear the canvas
// Visualization code...
}
// Error event listener for the audio element
audio.addEventListener('error', function(e) {
console.error('Error with audio playback:', e);
});
// Attempt to play the audio and start visualization when the audio can play through
audio.addEventListener('canplaythrough', function() {
startOrResumeContext();
renderFrame(); // Start the visualizer
});
});

View File

@ -152,11 +152,11 @@
{% endblock inline_javascript %}
{% if request.user.is_authenticated %}
<script>
{% if request.is_secure %}
let socket = new WebSocket(`wss://{{ request.get_host }}/ws/notifications/`);
{% else %}
let socket = new WebSocket(`ws://{{ request.get_host }}/ws/notifications/`);
{% endif %}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host; // Assuming host includes the port if needed
const socketPath = `${protocol}//${host}/ws/notifications/`;
let notification_socket = new WebSocket(socketPath);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
@ -211,18 +211,17 @@
toastBootstrap.show()
}
socket.onmessage = fn
socket.onclose = async function(event) {
notification_socket.onmessage = fn
notification_socket.onclose = async function(event) {
console.log("Notifications socket disconnected, reconnecting...")
let socketClosed = true;
await sleep(5000)
while (socketClosed) {
{# TODO: reconnect socket here #}
try {
let cl = socket.onclose
socket = new WebSocket(`ws://127.0.0.1:8000/ws/notifications/`);
socket.onmessage = fn
socket.onclose = cl
let cl = notification_socket.onclose
notification_socket = new WebSocket(socketPath);
notification_socket.onmessage = fn
notification_socket.onclose = cl
socketClosed = false
} catch (e) {
console.log("Can't connect to socket, reconnecting...")

View File

@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% load static %}
{% load crispy_forms_tags %}
{% block javascript %}
<script src="{% static 'js/jquery.min.js' %}"></script>
{% endblock %}
{% block content %}
<form method="post" action="{% url 'gallery:upload' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ form.management_form }}
{% for field in form %}
{{ field | as_crispy_field }}
{% endfor %}
<button type="submit">Upload</button>
</form>
<div id="progress-bar" style="width:0; height:20px; background:green;"></div>
<script>
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(form);
const xhr = new XMLHttpRequest();
xhr.open('POST', form.action, true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percentage = (e.loaded / e.total) * 100;
document.getElementById('progress-bar').style.width = percentage + '%';
}
};
xhr.onload = function() {
if (this.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed:', this.responseText);
}
};
xhr.send(formData);
});
</script>
{% endblock %}

View File

@ -1 +1,36 @@
{% extends 'base.html' %}
{% block css %}
<style>
.gallery {
display: flex;
flex-wrap: wrap;
justify-content: start; /* Align items to the start of the container */
align-items: stretch; /* Stretch items to fill the container */
}
.gallery-item {
/* Adjust the margin as needed */
margin: 5px;
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto; /* Automatically adjust the basis based on the content size */
}
.gallery-item img {
width: 100%; /* Make image responsive */
height: auto; /* Maintain aspect ratio */
display: block; /* Remove bottom space under the image */
}
</style>
{% endblock %}
{% block content %}
<div class="gallery">
{% for image in object_list %}
<div class="gallery-item">
<img src="{{ image.url }}" alt="Image">
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1 @@
{% extends 'base.html' %}

View File

@ -1,330 +1,77 @@
{% extends 'base.html' %}
{% load static %}
{% block css %}
<style>
body {
background-color: #EDEDED;
background-color: #f8f9fa;
}
h1 {
font-family: "Open Sans", sans-serif;
font-size: 13pt;
font-weight: 600;
text-transform: uppercase;
color: white;
cursor: default;
#song-info .card {
margin-bottom: 30px;
}
h4 {
font-family: "Open Sans", sans-serif;
font-size: 8pt;
font-weight: 400;
cursor: default;
#history .card {
margin-bottom: 15px;
}
h2 {
font-family: "Open Sans", sans-serif;
font-size: 13pt;
font-weight: 300;
color: white;
cursor: default;
#admin-controls {
margin-bottom: 30px;
}
.player {
height: 190px;
width: 430px;
background-color: #1E2125;
position: absolute;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
}
.player ul {
list-style: none;
}
.player ul li {
display: inline-block;
.card-img-top {
width: 100%;
height: 200px; /* You can adjust as needed */
object-fit: cover;
}
.cover {
position: absolute;
top: 0;
left: 0;
}
.cover img {
height: 190px;
width: 190px;
.audio-player {
width: 100%;
margin-top: 10px;
}
.info h1 {
margin-top: 15px;
margin-left: 180px;
line-height: 0;
}
.info h4 {
margin-left: 180px;
line-height: 20px;
color: #636367;
}
.info h2 {
margin-left: 180px;
.range-slider {
width: 100%;
margin-top: 10px;
}
.button-items {
margin-left: 180px;
}
#slider {
width: 182px;
height: 4px;
background: #151518;
border-radius: 2px;
}
#slider div {
width: 4px;
height: 4px;
margin-top: 1px;
background: #EF6DBC;
border-radius: 2px;
}
#timer {
color: #494B4E;
line-height: 0;
font-size: 9pt;
float: right;
font-family: Arial, Sans-Serif;
}
.controls {
margin-top: 20px;
}
.controls svg:nth-child(2) {
margin-left: 5px;
margin-right: 5px;
}
#play {
padding: 0 3px;
width: 30px;
height: 30px;
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
}
#play g {
stroke: #FEFEFE;
stroke-width: 1;
stroke-miterlimit: 10;
}
#play g path {
fill: #FEFEFE;
}
#play:hover {
cursor: pointer;
}
#play:hover g {
stroke: #8F4DA9;
cursor: pointer;
}
#play:hover g path {
fill: #9b59b6;
cursor: pointer;
}
.step-backward {
width: 18px;
height: 18px;
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
margin-bottom: 5px;
}
.step-backward g polygon {
fill: #FEFEFE;
}
.step-foreward {
width: 18px;
height: 18px;
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
margin-bottom: 5px;
}
.step-foreward g polygon {
fill: #FEFEFE;
}
#pause {
x: 0px;
y: 0px;
enable-background: new 0 0 25 25;
width: 30px;
height: 30px;
position: absolute;
margin-left: -38px;
cursor: pointer;
}
#pause rect {
fill: white;
}
#pause:hover rect {
fill: #8F4DA9;
}
.step-backward g polygon:hover, .step-foreward g polygon:hover {
fill: #EF6DBC;
cursor: pointer;
}
#skip p {
color: #2980b9;
}
#skip p:hover {
color: #e74c3c;
cursor: pointer;
}
.expend {
padding: 0.5px;
cursor: pointer;
}
.expend svg:hover g polygon {
fill: #EF6DBC;
.range-slider input[type="range"] {
width: 100%;
}
</style>
{% endblock %}
{% block content %}
<div class="player">
<ul>
<li class="cover"><img id="cover" src="" alt=""/></li>
<li class="info">
<h1 id="artist"></h1>
<h4 id="album"></h4>
<h2 id="name">I Need You Back</h2>
<div class="button-items">
<audio id="music">
<source id="music-src" type="audio/mp3">
<div class="container mt-5 music-body">
<!-- Music Player and Visualization -->
<div class="music-player-container">
<canvas id="audio-visualization" width="800" height="100"></canvas>
<audio id="audio-player" controls>
<source src="" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<div id="slider"><div id="elapsed"></div></div>
<p id="timer">0:00</p>
<div class="controls">
<span class="expend"><svg class="step-backward" viewBox="0 0 25 25" xml:space="preserve">
<g><polygon points="4.9,4.3 9,4.3 9,11.6 21.4,4.3 21.4,20.7 9,13.4 9,20.7 4.9,20.7"/></g>
</svg></span>
</div>
<svg id="play" viewBox="0 0 25 25" xml:space="preserve">
<defs><rect x="-49.5" y="-132.9" width="446.4" height="366.4"/></defs>
<g><circle fill="none" cx="12.5" cy="12.5" r="10.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.7,6.9V18c0,0,0.2,1.4,1.8,0l8.1-4.8c0,0,1.2-1.1-1-2L9.8,6.5 C9.8,6.5,9.1,6,8.7,6.9z"/>
</g>
</svg>
<svg id="pause" viewBox="0 0 25 25" xml:space="preserve">
<g>
<rect x="6" y="4.6" width="3.8" height="15.7"/>
<rect x="14" y="4.6" width="3.9" height="15.7"/>
</g>
</svg>
<span class="expend"><svg class="step-foreward" viewBox="0 0 25 25" xml:space="preserve">
<g><polygon points="20.7,4.3 16.6,4.3 16.6,11.6 4.3,4.3 4.3,20.7 16.7,13.4 16.6,20.7 20.7,20.7"/></g>
</svg></span>
<!-- Now Playing Information -->
<div class="now-playing">
<div class="track-image">
<img id="track-image" src="" alt="Track Image">
</div>
<div class="artist-info">
<!-- Dynamically populate artist info -->
</div>
</div>
</li>
</ul>
<div id="admin-controls" class="mb-3" style="display: none;">
<!-- Admin Controls will be inserted here -->
</div>
{% endblock %}
{% block inline_javascript %}
<script>
const music = document.getElementById("music");
async function playAudio() {
try {
await music.play();
playButton.style.visibility = "hidden";
pause.style.visibility = "visible";
} catch (err) {
playButton.style.visibility = "visible";
pause.style.visibility = "hidden";
}
}
const music_src = document.getElementById("music-src")
const cover_src = document.getElementById("cover")
const name = document.getElementById("name")
const album = document.getElementById("album")
const artist = document.getElementById("artist")
const socket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/radio/'
);
socket.onmessage = function(e) {
const data = JSON.parse(e.data);
console.log(data)
music_src.src = data.file
if (data.image !== null) {
cover_src.src = data.image
}
name.innerText = data.name
playAudio()
};
socket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
const playButton = document.getElementById("play");
const pauseButton = document.getElementById("pause");
const playhead = document.getElementById("elapsed");
const timeline = document.getElementById("slider");
const timer = document.getElementById("timer");
let duration;
pauseButton.style.visibility = "hidden";
const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
music.addEventListener("timeupdate", timeUpdate, false);
function timeUpdate() {
const playPercent = timelineWidth * (music.currentTime / duration);
playhead.style.width = playPercent + "px";
const secondsIn = Math.floor(((music.currentTime / duration) / 3.5) * 100);
if (secondsIn <= 9) {
timer.innerHTML = "0:0" + secondsIn;
} else {
timer.innerHTML = "0:" + secondsIn;
}
}
playButton.onclick = function() {
playAudio()
}
pauseButton.onclick = function() {
music.pause();
playButton.style.visibility = "visible";
pause.style.visibility = "hidden";
}
music.addEventListener("canplaythrough", function () {
duration = music.duration;
}, false);
</script>
<h2>History</h2>
<div id="history" class="mb-3">
<!-- Song History will be inserted here -->
</div>
</div>
<script src="{% static 'js/jquery.js' %}"></script>
<script src="{% static 'js/ws_script.js' %}"></script>
{% endblock %}

View File

@ -3,7 +3,7 @@
from rest_framework import generics, permissions, status, views
from rest_framework.response import Response
from akarpov.common.api import SmallResultsSetPagination
from akarpov.common.api.pagination import SmallResultsSetPagination
from akarpov.common.jwt import sign_jwt
from akarpov.users.api.serializers import (
UserEmailVerification,

View File

@ -24,6 +24,10 @@
"users/",
include("akarpov.users.api.urls", namespace="users"),
),
path(
"gallery/",
include("akarpov.gallery.api.urls", namespace="gallery"),
),
path(
"notifications/",
include("akarpov.notifications.providers.urls", namespace="notifications"),