Merge branch 'feature/marietje-4.1-in-header' into 'marietje-zuid'

Marietje in header and fix load times for upload page

Adds the Marietje header back into the mobile view. Also fixes an issue
with the upload page loading taking a long time due to a large id3
library.
Finally, this fixes errors running the pipeline: the project name is
changed to the folder name (marietje) and a typo was removed from one
of the maintainer names.

Closes #39, #37, and #44

See merge request technicie/MarietjeDjango!65
This commit is contained in:
2024-02-21 15:40:41 +01:00
6 changed files with 186 additions and 132 deletions

View File

@ -15,7 +15,10 @@
<link href="{% static 'fontawesomefree/css/all.min.css' %}" rel="stylesheet" type="text/css">
<!-- Vue JS -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
</script>
<!-- TaTa.js notifications -->
<script src="{% static 'marietje/js/tata.js' %}"></script>
@ -37,6 +40,7 @@
</section>
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand d-block d-lg-none" href="{% url "index" %}">Marietje 4.1</a>
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasNavbar" aria-controls="offcanvasNavbar">
<span class="navbar-toggler-icon"></span>

View File

@ -39,26 +39,28 @@
</li>
</ul>
<ul v-if="'start_personal_queue' in infobar && infobar.start_personal_queue !== null" id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<li v-if="infobar.start_personal_queue != 0" class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts in <% infobar.start_personal_queue.secondsToMMSS() %>
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts at <% (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() %>
</p>
</li>
<li class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends in <% infobar.end_personal_queue.secondsToMMSS() %>
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends at <% (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() %>
</p>
</li>
<li class="nav-item">
<p class="navbar-text mb-0 duration-queue" v-bind:class="{danger: infobar.length_personal_queue > infobar.max_length * 60}">(<% infobar.length_personal_queue.secondsToMMSS() %>)</p>
</li>
<ul id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<template v-if="infobar !== null && 'start_personal_queue' in infobar && infobar.start_personal_queue !== null">
<li v-if="infobar.start_personal_queue !== 0" class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts in ${ infobar.start_personal_queue.secondsToMMSS() }$
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts at ${ (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() }$
</p>
</li>
<li class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends in ${ infobar.end_personal_queue.secondsToMMSS() }$
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends at ${ (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() }$
</p>
</li>
<li class="nav-item">
<p class="navbar-text mb-0 duration-queue" v-bind:class="{danger: infobar.length_personal_queue > infobar.max_length * 60}">(${ infobar.length_personal_queue.secondsToMMSS() }$)</p>
</li>
</template>
</ul>
</div>
</nav>
@ -87,22 +89,22 @@
<tr :class="{ marietjequeue: (song.user === null),
underline_cell: (index === queue[-1]),
currentsong: (index === 0),}">
<td class="artist"><% song.song.artist %></td>
<td class="title"><% song.song.title %></td>
<td class="artist">${ song.song.artist }$</td>
<td class="title">${ song.song.title }$</td>
<td class="d-sm-table-cell d-none requested-by">
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
<% song.user.name %>
${ song.user.name }$
</template>
</td>
<td class="d-sm-table-cell d-none plays-at" style="text-align: right">
<template v-if="song.time_until_song_seconds !== null && song.time_until_song_seconds > 0 && playsIn === true">
<% song.time_until_song_seconds.secondsToMMSS() %>
${ song.time_until_song_seconds.secondsToMMSS() }$
</template>
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
<% song.plays_at.timestampToHHMMSS() %>
${ song.plays_at.timestampToHHMMSS() }$
</template>
</td>
<td>
@ -177,7 +179,7 @@
</select>
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
<template v-for="(i, index) in number_of_pages">
<option :value="i"><% i %></option>
<option :value="i">${ i }$</option>
</template>
</select>
</th>
@ -188,21 +190,21 @@
<template v-for="(song, index) in songs">
<tr>
<td>
<% song.artist %>
${ song.artist }$
</td>
<td>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none"><% song.title %></button>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none">${ song.title }$</button>
</td>
<td>
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
<% song.user.name %>
${ song.user.name }$
</template>
</td>
<td>
<% song.duration.secondsToMMSS() %>
${ song.duration.secondsToMMSS() }$
</td>
<td>
<button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none">
@ -226,18 +228,34 @@
const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }};
</script>
<script>
const queue_vue = new Vue({
el: '#queue-container',
delimiters: ['<%', '%>'],
data: {
current_song: null,
queue: [],
user_data: null,
refreshing: true,
refreshTimer: null,
clockInterval: null,
started_at: null,
playsIn: true,
const personal_queue_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
infobar: null,
}
},
}).mount('#personal-queue-container');
const queue_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
current_song: null,
queue: [],
user_data: null,
refreshing: true,
refreshTimer: null,
clockInterval: null,
started_at: null,
playsIn: true,
}
},
watch: {
playsIn: {
handler(val, oldVal) {
this.update_infobar();
}
},
},
mounted() {
this.clockInterval = setInterval(this.update_song_times, 1000);
@ -303,16 +321,14 @@
plays_in: this.playsIn,
now_in_seconds: 0,
}
const now_in_seconds = Math.round((new Date()).getTime() / 1000);
infoBar.now_in_seconds = now_in_seconds;
let current_song_played = now_in_seconds - this.queue[0].started_at;
infoBar.now_in_seconds = Math.round((new Date()).getTime() / 1000);
// If the current song is the current user's, their queue has started.
if (this.queue[0].user.id == this.user_data.id) {
if (this.queue[0].user.id === this.user_data.id) {
infoBar.start_personal_queue = 0;
}
for (let i = 0; i < this.queue.length; i++) {
const current_song = this.queue[i];
if (i == 0) {
if (i === 0) {
const current_song_remaining_seconds = current_song.song.duration - this.queue[1].time_until_song_seconds;
infoBar['length_personal_queue'] -= current_song_remaining_seconds;
infoBar['length_total_queue'] -= current_song_remaining_seconds;
@ -322,11 +338,11 @@
infoBar['length_personal_queue'] += current_song.song.duration;
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
if (infoBar['start_personal_queue'] === null) {
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds;
}
}
}
this.$emit("infobar", infoBar);
personal_queue_vue.infobar = infoBar;
},
refresh() {
if (!this.refreshing) {
@ -412,29 +428,20 @@
});
},
}
});
const personal_queue_vue = new Vue({
el: '#personal-queue-container',
delimiters: ['<%', '%>'],
data: {
infobar: [],
},
mounted() {
queue_vue.$on("infobar", infoBar => this.infobar = infoBar);
}
});
}).mount("#queue-container");
</script>
<script>
const request_vue = new Vue({
el: '#request-container',
delimiters: ['<%', '%>'],
data: {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
const request_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
}
},
watch: {
search_input: {
@ -538,7 +545,8 @@
"Content-Type": 'application/json',
},
}).then(response => {
if (response.status === 200) {
// TODO: Communicate failure through HTTP error codes (403) instead of checking response.success.
if (response.status === 200 && response.success) {
return response.json();
} else {
throw response;
@ -597,7 +605,7 @@
this.page_number = page_number;
}
}
});
}).mount('#request-container');
</script>
<script>
function volume_down() {

View File

@ -86,7 +86,7 @@ class SongUploadAPIView(APIView):
song = upload_file(file, artist, title, request.user)
upload_counter.inc()
return Response(status=200, data=self.serializer_class(song).data)
except UploadException:
except (UploadException, ConnectionRefusedError):
return Response(
status=500,
data={

View File

@ -41,7 +41,7 @@
</select>
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
<template v-for="(i, index) in number_of_pages">
<option :value="i"><% i %></option>
<option :value="i">${ i }$</option>
</template>
</select>
</th>
@ -52,10 +52,10 @@
<template v-for="(song, index) in songs">
<tr>
<td>
<% song.artist %>
${ song.artist }$
</td>
<td>
<a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);"><% song.title %></a>
<a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);">${ song.title }$</a>
</td>
</tr>
</template>
@ -64,17 +64,18 @@
</div>
</div>
<script>
let manage_vue = new Vue({
el: '#request-table',
delimiters: ['<%', '%>'],
data: {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
user_data: null,
let manage_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
user_data: null,
}
},
watch: {
search_input: {
@ -167,6 +168,6 @@
this.page_number = page_number;
}
}
});
}).mount('#request-table');
</script>
{% endblock %}

View File

@ -16,9 +16,12 @@
{% csrf_token %}
<div class="fileupload fileupload-new" data-provides="fileupload">
<span class="btn btn-primary btn-file">
<span v-if="fileObjects.length === 0">
<span v-if="fileObjects.length === 0 && !files_loading">
Select files
</span>
<span v-else-if="files_loading">
Loading new files...
</span>
<span v-else>
Change
</span>
@ -29,26 +32,35 @@
<div class="songs">
<div v-for="fileObject in fileObjects" class="song-container card mb-3">
<div class="card-header">
<h3><% fileObject.name %></h3>
<h3>${ fileObject.name }$</h3>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div v-if="fileObject.artist === '' || fileObject.artist === null" class="alert alert-danger">Please enter an artist for this song.</div>
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]" class="form-control input-sm artist" disabled
<div v-if="fileObject.artist === '' || fileObject.artist === null"
class="alert alert-danger">Please enter an artist for this song.
</div>
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]"
class="form-control input-sm artist" disabled
placeholder="Artist" v-model="fileObject.artist"/>
<input v-else type="text" name="artist[]" class="form-control input-sm artist"
<input v-else type="text" name="artist[]"
class="form-control input-sm artist"
placeholder="Artist" v-model="fileObject.artist"/>
</div>
<div class="form-group mb-3">
<div v-if="fileObject.title === '' || fileObject.title === null" class="alert alert-danger">Please enter a title for this song.</div>
<input v-if="upload_in_progress || uploaded" type="text" name="title[]" class="form-control input-sm title" disabled
<div v-if="fileObject.title === '' || fileObject.title === null"
class="alert alert-danger">Please enter a title for this song.
</div>
<input v-if="upload_in_progress || uploaded" type="text" name="title[]"
class="form-control input-sm title" disabled
placeholder="Title" v-model="fileObject.title"/>
<input v-else type="text" name="title[]" class="form-control input-sm title"
placeholder="Title" v-model="fileObject.title"/>
</div>
<template v-if="fileObject.upload_finished === true">
<div v-if="fileObject.success === true" class="alert alert-success">Upload finished successfully.</div>
<div v-else class="alert alert-danger"><% fileObject.error_message %></div>
<div v-if="fileObject.success === true" class="alert alert-success">Upload
finished successfully.
</div>
<div v-else class="alert alert-danger">${ fileObject.error_message }$</div>
</template>
</div>
</div>
@ -56,14 +68,20 @@
</div>
<div class="card-footer">
<div class="progress mt-2 mb-3">
<div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }" class="progress-bar progress-bar-striped" role="progressbar" :style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
<div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }"
class="progress-bar progress-bar-striped" role="progressbar"
:style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<template v-if="upload_in_progress || uploaded">
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">Clear</button>
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">
Clear
</button>
<button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button>
</template>
<template v-else>
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100" type="submit" value="Upload" v-on:click="upload"/>
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100"
type="submit" value="Upload" v-on:click="upload"/>
<button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button>
</template>
</div>
@ -73,20 +91,21 @@
</div>
</div>
<link rel="stylesheet" href="{% static 'songs/css/upload.css' %}"/>
<script type="module">
import * as id3 from '//unpkg.com/id3js@^2/lib/id3.js';
let upload_vue = new Vue({
el: '#uploadform',
delimiters: ['<%', '%>'],
data: {
files: [],
fileObjects: [],
uploaded: false,
upload_in_progress: false,
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
<script>
let upload_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
files: [],
fileObjects: [],
uploaded: false,
upload_in_progress: false,
files_loading: false,
}
},
computed: {
ready_for_upload: function() {
ready_for_upload: function () {
if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) {
return false;
} else {
@ -98,14 +117,14 @@
return true;
}
},
everything_successfully_uploaded: function() {
everything_successfully_uploaded: function () {
return this.fileObjects.map((fileObject) => {
return fileObject.upload_finished === true && fileObject.success === true;
}).reduce((previousValue, currentValue) => {
return previousValue && currentValue;
}, true);
},
progress_bar_width: function() {
progress_bar_width: function () {
if (this.fileObjects.length === 0) {
return 0;
}
@ -158,14 +177,20 @@
}).then(() => {
this.fileObjects[i].success = true;
}).catch(e => {
console.log(e);
if (e instanceof Response) {
e.json().then(data => {
this.fileObjects.error_message = data.errorMessage;
this.fileObjects.success = false;
});
try {
e.json().then(data => {
this.fileObjects[i].error_message = data.errorMessage;
this.fileObjects[i].success = false;
});
} catch {
this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects[i].success = false;
}
} else {
this.fileObjects.error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects.success = false;
this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects[i].success = false;
}
}).finally(() => {
this.fileObjects[i].upload_finished = true;
@ -177,23 +202,25 @@
});
},
async set_new_files(event) {
this.files_loading = true;
this.uploaded = false;
this.upload_in_progress = false;
this.files = event.target.files;
let newFileObjects = [];
for (let i = 0; i < this.files.length; i++) {
try {
const tags = await this.parseSong(this.files[i]);
await this.parseSong(this.files[i]).then((song) => {
newFileObjects.push(
{
"file": this.files[i],
"name": this.files[i].name,
"artist": tags.artist,
"title": tags.title,
"artist": song.artist,
"title": song.title,
"success": null,
"error_message": null,
"upload_finished": false,
}
);
} catch {
}).catch(() => {
newFileObjects.push(
{
"file": this.files[i],
@ -205,14 +232,28 @@
"upload_finished": false,
}
)
}
});
}
this.fileObjects = newFileObjects;
this.files_loading = false;
},
parseSong(file) {
return id3.fromFile(file);
async parseSong(file) {
let jsMediaTags = window.jsmediatags;
const tags = await new Promise((resolve, reject) => {
jsMediaTags.read(file, {
onSuccess: function (tag) {
resolve(tag);
},
onError: function (error) {
reject(error);
}
});
});
return tags.tags;
}
}
});
}).mount('#uploadform');
</script>
{% endblock %}

View File

@ -1,5 +1,5 @@
[tool.poetry]
name = "MarietjeDjango"
name = "marietje"
version = "4.1.0"
description = "A music player for the south canteen of the Huygens building"
authors = [
@ -11,7 +11,7 @@ authors = [
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
]
maintainers = [
"Kees van Kempen <ru@keesvankempen.nl",
"Kees van Kempen <ru@keesvankempen.nl>",
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
]
readme = "README.md"