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 rest_framework import serializers
from akarpov.blog.models import Comment, Post, Tag 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 from akarpov.users.api.serializers import UserPublicInfoSerializer

View File

@ -9,7 +9,7 @@
) )
from akarpov.blog.models import Post from akarpov.blog.models import Post
from akarpov.blog.services import get_main_rating_posts 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): 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 = models.ForeignKey(
"Collection", related_name="images", on_delete=models.CASCADE "Collection", related_name="images", on_delete=models.CASCADE
) )
public = models.BooleanField(default=False)
user = models.ForeignKey( user = models.ForeignKey(
"users.User", related_name="images", on_delete=models.CASCADE "users.User", related_name="images", on_delete=models.CASCADE
) )

View File

@ -2,6 +2,7 @@
from akarpov.gallery.views import ( from akarpov.gallery.views import (
collection_view, collection_view,
image_upload_view,
image_view, image_view,
list_collections_view, list_collections_view,
list_tag_images_view, list_tag_images_view,
@ -10,6 +11,7 @@
app_name = "gallery" app_name = "gallery"
urlpatterns = [ urlpatterns = [
path("", list_collections_view, name="list"), path("", list_collections_view, name="list"),
path("upload/", image_upload_view, name="upload"),
path("<str:slug>", collection_view, name="collection"), path("<str:slug>", collection_view, name="collection"),
path("tag/<str:slug>", list_tag_images_view, name="tag"), path("tag/<str:slug>", list_tag_images_view, name="tag"),
path("image/<str:slug>", image_view, name="view"), 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.shortcuts import get_object_or_404
from django.views import generic from django.views import generic
from akarpov.common.views import HasPermissions from akarpov.common.views import HasPermissions
from akarpov.gallery.forms import ImageUploadForm
from akarpov.gallery.models import Collection, Image, Tag from akarpov.gallery.models import Collection, Image, Tag
@ -46,3 +48,25 @@ class ImageView(generic.DetailView, HasPermissions):
image_view = ImageView.as_view() 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 drf_spectacular.utils import extend_schema_field
from rest_framework import serializers 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.music.models import Album, Author, Playlist, Song
from akarpov.users.api.serializers import UserPublicInfoSerializer from akarpov.users.api.serializers import UserPublicInfoSerializer

View File

@ -1,6 +1,6 @@
from rest_framework import generics, permissions 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 ( from akarpov.music.api.serializers import (
FullPlaylistSerializer, FullPlaylistSerializer,
ListSongSerializer, ListSongSerializer,

View File

@ -1,6 +1,6 @@
from rest_framework import generics, permissions 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.models import Notification
from akarpov.notifications.providers.site.api.serializers import ( from akarpov.notifications.providers.site.api.serializers import (
SiteNotificationSerializer, 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 %} {% endblock inline_javascript %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<script> <script>
{% if request.is_secure %} const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
let socket = new WebSocket(`wss://{{ request.get_host }}/ws/notifications/`); const host = window.location.host; // Assuming host includes the port if needed
{% else %} const socketPath = `${protocol}//${host}/ws/notifications/`;
let socket = new WebSocket(`ws://{{ request.get_host }}/ws/notifications/`);
{% endif %} let notification_socket = new WebSocket(socketPath);
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
@ -211,18 +211,17 @@
toastBootstrap.show() toastBootstrap.show()
} }
socket.onmessage = fn notification_socket.onmessage = fn
socket.onclose = async function(event) { notification_socket.onclose = async function(event) {
console.log("Notifications socket disconnected, reconnecting...") console.log("Notifications socket disconnected, reconnecting...")
let socketClosed = true; let socketClosed = true;
await sleep(5000) await sleep(5000)
while (socketClosed) { while (socketClosed) {
{# TODO: reconnect socket here #}
try { try {
let cl = socket.onclose let cl = notification_socket.onclose
socket = new WebSocket(`ws://127.0.0.1:8000/ws/notifications/`); notification_socket = new WebSocket(socketPath);
socket.onmessage = fn notification_socket.onmessage = fn
socket.onclose = cl notification_socket.onclose = cl
socketClosed = false socketClosed = false
} catch (e) { } catch (e) {
console.log("Can't connect to socket, reconnecting...") 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' %} {% 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' %} {% extends 'base.html' %}
{% load static %}
{% block css %} {% block css %}
<style> <style>
body { body {
background-color: #EDEDED; background-color: #f8f9fa;
} }
h1 { #song-info .card {
font-family: "Open Sans", sans-serif; margin-bottom: 30px;
font-size: 13pt;
font-weight: 600;
text-transform: uppercase;
color: white;
cursor: default;
} }
h4 { #history .card {
font-family: "Open Sans", sans-serif; margin-bottom: 15px;
font-size: 8pt;
font-weight: 400;
cursor: default;
} }
h2 { #admin-controls {
font-family: "Open Sans", sans-serif; margin-bottom: 30px;
font-size: 13pt;
font-weight: 300;
color: white;
cursor: default;
} }
.player { .card-img-top {
height: 190px; width: 100%;
width: 430px; height: 200px; /* You can adjust as needed */
background-color: #1E2125; object-fit: cover;
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;
} }
.cover { .audio-player {
position: absolute; width: 100%;
top: 0; margin-top: 10px;
left: 0;
}
.cover img {
height: 190px;
width: 190px;
} }
.info h1 { .range-slider {
margin-top: 15px; width: 100%;
margin-left: 180px; margin-top: 10px;
line-height: 0;
}
.info h4 {
margin-left: 180px;
line-height: 20px;
color: #636367;
}
.info h2 {
margin-left: 180px;
} }
.button-items { .range-slider input[type="range"] {
margin-left: 180px; width: 100%;
}
#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;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="player"> <div class="container mt-5 music-body">
<ul> <!-- Music Player and Visualization -->
<li class="cover"><img id="cover" src="" alt=""/></li> <div class="music-player-container">
<li class="info"> <canvas id="audio-visualization" width="800" height="100"></canvas>
<h1 id="artist"></h1> <audio id="audio-player" controls>
<h4 id="album"></h4> <source src="" type="audio/mpeg">
<h2 id="name">I Need You Back</h2> Your browser does not support the audio element.
<div class="button-items">
<audio id="music">
<source id="music-src" type="audio/mp3">
</audio> </audio>
<div id="slider"><div id="elapsed"></div></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>
<svg id="play" viewBox="0 0 25 25" xml:space="preserve"> <!-- Now Playing Information -->
<defs><rect x="-49.5" y="-132.9" width="446.4" height="366.4"/></defs> <div class="now-playing">
<g><circle fill="none" cx="12.5" cy="12.5" r="10.8"/> <div class="track-image">
<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"/> <img id="track-image" src="" alt="Track Image">
</g> </div>
</svg> <div class="artist-info">
<!-- Dynamically populate artist info -->
<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>
</div> </div>
</div> </div>
</li>
</ul> <div id="admin-controls" class="mb-3" style="display: none;">
<!-- Admin Controls will be inserted here -->
</div> </div>
{% endblock %}
<h2>History</h2>
{% block inline_javascript %} <div id="history" class="mb-3">
<!-- Song History will be inserted here -->
<script> </div>
const music = document.getElementById("music"); </div>
async function playAudio() { <script src="{% static 'js/jquery.js' %}"></script>
try { <script src="{% static 'js/ws_script.js' %}"></script>
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>
{% endblock %} {% endblock %}

View File

@ -3,7 +3,7 @@
from rest_framework import generics, permissions, status, views from rest_framework import generics, permissions, status, views
from rest_framework.response import Response 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.common.jwt import sign_jwt
from akarpov.users.api.serializers import ( from akarpov.users.api.serializers import (
UserEmailVerification, UserEmailVerification,

View File

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