mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 03:56:33 +03:00
updated gallery and music radio
This commit is contained in:
parent
4ef6021499
commit
e94f90d091
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
0
akarpov/common/api/__init__.py
Normal file
0
akarpov/common/api/__init__.py
Normal file
19
akarpov/common/api/pagination.py
Normal file
19
akarpov/common/api/pagination.py
Normal 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
|
12
akarpov/common/api/permissions.py
Normal file
12
akarpov/common/api/permissions.py
Normal 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
|
||||||
|
)
|
14
akarpov/common/api/serializers.py
Normal file
14
akarpov/common/api/serializers.py
Normal 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
|
0
akarpov/gallery/api/__init__.py
Normal file
0
akarpov/gallery/api/__init__.py
Normal file
24
akarpov/gallery/api/serializers.py
Normal file
24
akarpov/gallery/api/serializers.py
Normal 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},
|
||||||
|
}
|
9
akarpov/gallery/api/urls.py
Normal file
9
akarpov/gallery/api/urls.py
Normal 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"),
|
||||||
|
]
|
21
akarpov/gallery/api/views.py
Normal file
21
akarpov/gallery/api/views.py
Normal 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
40
akarpov/gallery/forms.py
Normal 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
|
||||||
|
)
|
17
akarpov/gallery/migrations/0004_image_public.py
Normal file
17
akarpov/gallery/migrations/0004_image_public.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
177
akarpov/static/js/ws_script.js
Normal file
177
akarpov/static/js/ws_script.js
Normal 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
|
||||||
|
});
|
||||||
|
});
|
|
@ -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...")
|
||||||
|
|
43
akarpov/templates/gallery/image_form.html
Normal file
43
akarpov/templates/gallery/image_form.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
1
akarpov/templates/gallery/upload.html
Normal file
1
akarpov/templates/gallery/upload.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{% extends 'base.html' %}
|
|
@ -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 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="admin-controls" class="mb-3" style="display: none;">
|
||||||
|
<!-- Admin Controls will be inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<svg id="pause" viewBox="0 0 25 25" xml:space="preserve">
|
<h2>History</h2>
|
||||||
<g>
|
<div id="history" class="mb-3">
|
||||||
<rect x="6" y="4.6" width="3.8" height="15.7"/>
|
<!-- Song History will be inserted here -->
|
||||||
<rect x="14" y="4.6" width="3.9" height="15.7"/>
|
</div>
|
||||||
</g>
|
</div>
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span class="expend"><svg class="step-foreward" viewBox="0 0 25 25" xml:space="preserve">
|
<script src="{% static 'js/jquery.js' %}"></script>
|
||||||
<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>
|
<script src="{% static 'js/ws_script.js' %}"></script>
|
||||||
</svg></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user