33 Commits

Author SHA1 Message Date
d17c6d8b18 api: Fix requestor in queue.png 2022-07-06 16:12:13 +02:00
ef627318ba api: Add image endpoint for use on tosti.science.ru.nl 2022-07-06 16:03:10 +02:00
4491fc234b Add beeldscherm
Closes #20

Co-authored-by: Gerdriaan Mulder <mrngm@moeilijklastig.nl>
2022-07-06 13:46:15 +02:00
b1a080799c Merge branch 'dsprenkels/gitignore' into 'marietje-zuid'
Add a more complete .gitignore file; NFC

See merge request dsprenkels/MarietjeDjango!58
2020-06-16 10:50:22 +02:00
e67bc8dc5a Merge branch 'dsprenkels/unlimited_queue_length' into 'marietje-zuid'
queues: Fix unlimited_queue codename in check

See merge request dsprenkels/MarietjeDjango!57
2020-06-16 10:38:35 +02:00
e5fe2aa1cf queues: Fix unlimited_queue codename in check 2020-06-16 10:34:02 +02:00
9529ae245a Add a more complete .gitignore file; NFC 2020-06-16 10:14:08 +02:00
a325ebbe82 Merge branch 'dsprenkels/songs-reportnote-filter' into 'marietje-zuid'
Add a filter on reportnote count in the Songs admin list

See merge request dsprenkels/MarietjeDjango!52
2020-06-15 18:08:26 +02:00
a422e6d4f5 Merge branch 'dsprenkels/admin-optimizations' into 'marietje-zuid'
admin: Use autocomplete for inline reportuser

See merge request dsprenkels/MarietjeDjango!56
2020-06-15 17:54:33 +02:00
b4a6530204 admin: Use autocomplete for inline reportuser 2020-06-15 17:52:20 +02:00
1292694c4a Merge branch 'dsprenkels/admin-optimizations' into 'marietje-zuid'
admin: Use autocomplete for user field

See merge request dsprenkels/MarietjeDjango!55
2020-06-15 17:39:28 +02:00
64b26d03a1 admin: Use autocomplete for user field
Before this patch, we used the default setting, which emits a HTML
<select> tag containing a list of *all* the users.  We currently
have enough users that we do not want to load that complete list
every time.  So now, use the autocomplete field instead.
2020-06-15 17:34:20 +02:00
2ade1a7dfa Merge branch 'marietje-zuid' into 'dsprenkels/songs-reportnote-filter'
# Conflicts:
#   marietje/songs/admin.py
2020-06-15 17:13:52 +02:00
1a797a5d98 Merge branch 'dsprenkels/notes-song-link' into 'marietje-zuid'
admin: reports: Hide song list

See merge request dsprenkels/MarietjeDjango!54
2020-06-15 17:11:06 +02:00
b604ac9955 admin: reports: Hide song list
In 1b5b510, the complete list of songs was re-added.  Loading this
list is super slow and should not happen.
2020-06-15 17:10:18 +02:00
bb6166c1db Merge branch 'notes-song-link' into 'marietje-zuid'
Add a link to relevant song in ReportNote admin interface

See merge request dsprenkels/MarietjeDjango!51
2020-06-15 16:31:35 +02:00
3aa876e223 Merge branch 'dsprenkels/freeze-pylint' into 'marietje-zuid'
Freeze pylint version

See merge request dsprenkels/MarietjeDjango!53
2020-06-15 16:27:50 +02:00
61fa646353 ci: Freeze pylint version
This will prevent the CI from "randomly" breaking every now and
then, because of added lints in pylint.  From now on, pylint is
updated manually.
2020-06-15 16:26:12 +02:00
416fb3e5a9 admin: songs: Add a filter on reportnote count
This filter allows the admin to list only the songs that have a
report note that needs to be resolved.
2020-06-15 16:14:10 +02:00
1b5b5106ba report-note: admin: Add a link to relevant song
Previously, the admin could not directly move from a report note to
its corresponding song. This commit adds a link that will go
directly to the "change" page for the corresponding song.
2020-06-15 15:18:37 +02:00
4a1df11b40 Merge branch 'oslomp/issue_11' into 'marietje-zuid'
api: Only allow POST requests on views.skip

Closes #11

See merge request dsprenkels/MarietjeDjango!41
2020-06-11 09:28:51 +02:00
f4ab85106d Merge branch 'unlimited_queue_length' into 'marietje-zuid'
Add 'unlimited_queue_length' permission

See merge request dsprenkels/MarietjeDjango!50
2020-06-10 11:13:06 +02:00
23f651bbd1 Fix pylint errors 2020-06-09 17:57:25 +02:00
3724b94e4a Add 'unlimited_queue_length' permission
The 'unlimited_queue_length' permission allows a user to queue
songs without being restricted by the 45-minute queue length.
2020-06-09 17:37:08 +02:00
4fdf25ac43 Merge branch 'marietje-zuid' into 'marietje-zuid'
stats: show 'last updated' in full width on mobile

See merge request dsprenkels/MarietjeDjango!49
2020-03-28 16:19:43 +01:00
83406ec0ab stats: show 'last updated' in full width on mobile 2020-03-28 16:19:16 +01:00
e447a7c210 Merge branch 'marietje-zuid' into 'marietje-zuid'
user-stats: fix layout, fixes #13

Closes #13

See merge request dsprenkels/MarietjeDjango!48
2020-03-28 15:59:06 +01:00
91d3b0cf35 user-stats: fix layout, fixes #13 2020-03-28 15:54:26 +01:00
228d0208f2 Merge branch 'marietje-zuid' into 'marietje-zuid'
stats: show equal height columns

See merge request dsprenkels/MarietjeDjango!47
2020-03-28 15:46:47 +01:00
62ba17ef67 stats: show equal height columns 2020-03-28 15:43:02 +01:00
6a549fbd7b Merge branch 'oslomp/quickfix_artists' into 'marietje-zuid'
quickfix artist stat

See merge request dsprenkels/MarietjeDjango!46
2020-03-28 15:11:44 +01:00
371334326b quickfix artist stat 2020-03-28 15:12:09 +01:00
53d3c6e5c4 Closes #11 2019-10-26 08:48:33 +02:00
17 changed files with 501 additions and 175 deletions

141
.gitignore vendored
View File

@ -1,3 +1,138 @@
/venv/
*.pyc
*.pyo
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

View File

@ -9,7 +9,7 @@ pylint:
- apt-get -qq install -y python3 python3-venv python3-pip
- python3 -m venv venv
- source venv/bin/activate
- pip install -r requirements.txt pylint
- pip install -r requirements.txt
script:
- pylint marietje/marietje marietje/metrics marietje/playerapi marietje/queues marietje/songs marietje/stats

View File

@ -19,4 +19,5 @@ urlpatterns = [
url(r'^volumedown', views.volume_down),
url(r'^volumeup', views.volume_up),
url(r'^mute', views.mute),
url(r'^hier-heb-je-je-endpoint-voor-tosti.png$', views.queue_png),
]

View File

@ -8,7 +8,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Q, Sum, Value
from django.db.models.functions import Coalesce
from django.http import JsonResponse, HttpResponseForbidden
from django.http import JsonResponse, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_http_methods
@ -189,6 +189,7 @@ def queue(request):
return JsonResponse(json)
@require_http_methods(["POST"])
@api_auth_required
def skip(request):
playlist_song = request.user.queue.current_song()
@ -348,3 +349,26 @@ def _request_weight(ps):
return float(ps.song.duration)
# Count other requests for 10%
return 0.10 * float(ps.song.duration)
def queue_png(request):
current_song = request.user.queue.current_song()
requestor = 'privacy™' if current_song.user else 'Marietje'
artist, title = current_song.song.artist, current_song.song.title
from PIL import Image, ImageDraw, ImageFont
width, height = 640, 480
ttf = 'marietje/static/fonts/comic-serif.tff'
zuidSerifRequestor = ImageFont.truetype(ttf, 64)
zuidSerifArtist = ImageFont.truetype(ttf, 80)
zuidSerifTitle = ImageFont.truetype(ttf, 64)
img = Image.new('RGB', (width, height), color='#BE311A')
imgDraw = ImageDraw.Draw(img)
imgDraw.text((10, 110), requestor, fill='#FFFFFF', font=zuidSerifRequestor)
imgDraw.text((10, 200), artist, fill='#FFFFFF', font=zuidSerifArtist)
imgDraw.text((10, 280), title, fill='#FFFFFF', font=zuidSerifTitle)
response = HttpResponse(content_type='image/png')
img.save(response, 'png')
return response

View File

@ -49,3 +49,13 @@ tr.requested_song{
background-color: #f9f9f9;
border-bottom: 2px solid #777777;
}
/* Bootstrap 3 doesn't support equal height columns, hack via <https://medium.com/wdstack/bootstrap-equal-height-columns-d07bc934eb27#892f> */
.row.display-flex {
display: flex;
flex-wrap: wrap;
}
.row.display-flex > [class*='col-'] {
display: flex;
flex-direction: column;
}

Binary file not shown.

View File

@ -37,6 +37,7 @@
<li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">Stats</a></li>
{% url 'stats:user_stats' as url %}
<li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">User Stats</a></li>
<li><a href="/beeldscherm/">Beeldscherm</a></li>
{% if user.is_staff %}
{% url 'admin:index' as url %}
<li><a href="{{ url }}">Admin</a></li>

View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- marietje-zuid -->
<title>Marietje-Zuid beeldscherm</title>
<style>
@font-face {
font-family: zuidSerif;
src: url('/static/fonts/comic-serif.tff') format('truetype');
}
html, body {
margin: 0;
padding: 0;
background-color: #BE311A;
color: #FFFFFF;
font-family: zuidSerif;
overflow: hidden;
}
p {
margin: 0;
padding: 0;
white-space: nowrap;
}
.artist {
font-size: 80pt;
}
.title {
font-size: 64pt;
}
.requestedBy {
font-size: 64pt;
}
#content {
position: absolute;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
flex-direction: column;
padding-left: 25px;
}
</style>
<script>
function init() {
fetchCurrentQueue();
}
function fetchCurrentQueue() {
fetch('/api/queue')
.then(response => {
switch(response.status) {
case 200:
break;
case 401:
throw { name: 'NotLoggedIn', message: 'Fblwurp!' };
default:
throw new Error('unexpected response: '+ response);
}
return response.json();
})
.then(json => {
updateScreen(json.current_song.requested_by, json.current_song.song.artist, json.current_song.song.title);
setTimeout(fetchCurrentQueue, 1000);
}).catch(err => {
if(err.name == "NotLoggedIn") {
setTimeout(function() {
window.location.assign('/login/?next=/beeldscherm/');
}, 5000);
updateScreen('Error Handler', 'Not Logged In', 'Redirecting You In Five');
return;
}
console.log("error: "+ err);
updateScreen('Error Handler', 'Faulty Request', 'Help!');
})
}
function updateScreen(requestor, artist, title) {
var r = document.getElementById('requestor');
var a = document.getElementById('song_artist');
var t = document.getElementById('song_title');
r.textContent = requestor;
a.textContent = artist;
t.textContent = title;
}
document.addEventListener('DOMContentLoaded', function() {
init();
}, false);
</script>
</head>
<body>
<div id="content">
<p class="requestedBy" id="requestor">?</p>
<p class="artist" id="song_artist">?</p>
<p class="title" id="song_title">?</p>
</div>
</body>
</html>

View File

@ -40,4 +40,5 @@ urlpatterns = [
url(r'^playerapi/', include('playerapi.urls')),
url(r'^stats/', include('stats.urls')),
url(r'^metrics', metrics, name='metrics'),
url(r'^beeldscherm/$', partial(render, template_name='beeldscherm.html'), name='beeldscherm'),
]

View File

@ -12,8 +12,10 @@ class OrderAdmin(admin.ModelAdmin):
@admin.register(PlaylistSong)
class PlaylistSongAdmin(admin.ModelAdmin):
list_display = ('playlist', 'song', 'user', 'state', 'played_at')
list_display_links = ('song',)
list_filter = ('playlist', 'state', 'user')
search_fields = ('song__title', 'song__artist', 'user__name')
autocomplete_fields = ('user',)
readonly_fields = ('song',)

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.13 on 2020-06-09 15:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('queues', '0008_remove_queuecommand_executed'),
]
operations = [
migrations.AlterModelOptions(
name='queue',
options={'permissions': (('can_skip', 'Can skip the currently playing song'), ('can_move', 'Can move all songs in the queue'), ('can_cancel', 'Can cancel all songs in the queue'), ('can_control_volume', 'Can control the volume of Marietje'), ('unlimited_queue_length', 'Is unlimited by maximum queue length'))},
),
]

View File

@ -64,6 +64,7 @@ class Queue(models.Model):
('can_move', 'Can move all songs in the queue'),
('can_cancel', 'Can cancel all songs in the queue'),
('can_control_volume', 'Can control the volume of Marietje'),
('unlimited_queue_length', 'Is unlimited by maximum queue length'),
)
name = models.TextField()
@ -111,7 +112,7 @@ class Queue(models.Model):
return songs[1:]
def request(self, song, user):
if not user.is_superuser:
if not user.has_perm('queues.unlimited_queue_length'):
playlist_songs = PlaylistSong.objects.filter(playlist=self.playlist, state=0).order_by('id')
seconds_in_a_row = sum(ps.song.duration for ps in playlist_songs if ps.user == user)
@ -149,7 +150,7 @@ class Queue(models.Model):
song_count += 1
def __str__(self):
return self.name
return str(self.name)
class QueueCommand(models.Model):
@ -161,4 +162,4 @@ class QueueCommand(models.Model):
command = models.TextField()
def __str__(self):
return self.command
return str(self.command)

View File

@ -1,4 +1,7 @@
from django.contrib import admin
from django.db.models import Count
from django.urls import reverse
from django.utils.html import format_html
from .models import ReportNote, Song
@ -6,16 +9,40 @@ from .models import ReportNote, Song
class ReportNoteInline(admin.StackedInline):
model = ReportNote
extra = 0
autocomplete_fields = ('user',)
class SongHasReportNoteFilter(admin.SimpleListFilter):
title = 'report notes'
parameter_name = 'reportnotes'
def lookups(self, request, model_admin):
return (
('yes', 'yes'),
('no', 'no'),
)
def queryset(self, request, queryset):
queryset = queryset.annotate(num_reports=Count('reportnote'))
if self.value() == 'yes':
return queryset.exclude(num_reports=0)
if self.value() == 'no':
return queryset.filter(num_reports=0)
return queryset
@admin.register(Song)
class SongAdmin(admin.ModelAdmin):
list_display = ('artist', 'title', 'user_name', 'reports')
list_display_links = ('artist', 'title')
list_filter = (SongHasReportNoteFilter,)
search_fields = ('artist', 'title', 'user__name')
inlines = [ReportNoteInline]
autocomplete_fields = ('user',)
@staticmethod
def reports(song):
return ReportNote.objects.filter(song=song).count()
# num_reports is annotated by SongHasReportNoteFilter
return song.num_reports
@staticmethod
def user_name(song):
@ -30,6 +57,15 @@ class SongAdmin(admin.ModelAdmin):
@admin.register(ReportNote)
class ReportNoteAdmin(admin.ModelAdmin):
exclude = ('song',)
list_display = ('song', 'note', 'user')
search_fields = ('song__artist', 'song__title', 'user__name')
readonly_fields = ('song',)
autocomplete_fields = ('user',)
readonly_fields = ('song_link',)
@staticmethod
def song_link(note):
url = reverse("admin:songs_song_change", args=(note.song.id,))
return format_html("<a href='{url}'>{song}</a>", url=url, song=note.song)
song_link.short_description = "Song link"

View File

@ -5,14 +5,14 @@
{% block content %}
<h1>Statistics</h1>
<div class="row">
<div class="row display-flex">
{% if not stats %}
<div class="alert alert-danger">
<div class="col-xs-12 alert alert-danger">
<strong>Stats unavailable :(</strong>
</div>
{% else %}
{% if current_age_text %}
<div class="alert alert-info">
<div class="col-xs-12 alert alert-info">
<strong>{{ current_age_text }}</strong>
{% endif %}
</div>

View File

@ -7,180 +7,178 @@
{% block content %}
<h1>User Statistics</h1>
<div class="row">
<div class="row display-flex">
{% if not stats %}
<div class="alert alert-danger">
<div class="col-xs-12 alert alert-danger">
<strong>Stats unavailable :(</strong>
</div>
{% else %}
{% if current_age_text %}
<div class="alert alert-info">
<div class="col-xs-12 alert alert-info">
<strong>{{ current_age_text }}</strong>
</div>
{% endif %}
<div class="col-md-6">
<h2>Most played songs</h2>
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different
songs a total of <strong> {{ stats.total_requests }} </strong> times. This
means <strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests have been unique. </p>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_songs %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: middle;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played Artists</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_artists %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played songs</h2>
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different
songs a total of <strong> {{ stats.total_requests }} </strong> times. This
means <strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests have been unique. </p>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_songs %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: middle;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-6">
<h2>Uploads requested</h2>
<p> You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
shows how many times these have been requested by other people. The right column shows
how many times you requested your own songs. In total your songs
have been queued <strong> {{stats.total_played_uploads }} </strong> times by others and
<strong> {{stats.total_played_user_uploads }} </strong> by yourself.
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th style="text-align: right;">Others</th>
<th>You</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploads %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>{{ stat.user_total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-6">
<h2>Most played Artists</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_artists %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Upload artists requested</h2>
<p> The left column shows how many times songs from artists uploaded by you have been requested by
other people. The right column shows how many times you requested those songs.
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th style="text-align: right;">Others</th>
<th>You</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploaded_artists %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>{{ stat.user_total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-6">
<h2>Uploads requested</h2>
<p> You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
shows how many times these have been requested by other people. The right column shows
how many times you requested your own songs. In total your songs
have been queued <strong> {{stats.total_played_uploads }} </strong> times by others and
<strong> {{stats.total_played_user_uploads }} </strong> by yourself.
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th style="text-align: right;">Others</th>
<th>You</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploads %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>{{ stat.user_total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played uploaders</h2>
<p> The people whose songs you have queued the most are:</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Uploader</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploaders %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__user__name }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>({% widthratio stat.total stats.total_requests 100 %}%)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-6">
<h2>Upload artists requested</h2>
<p> The left column shows how many times songs from artists uploaded by you have been requested by
other people. The right column shows how many times you requested those songs.
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th style="text-align: right;">Others</th>
<th>You</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploaded_artists %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>{{ stat.user_total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played uploaders</h2>
<p> The people whose songs you have queued the most are:</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Uploader</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploaders %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__user__name }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>({% widthratio stat.total stats.total_requests 100 %}%)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Biggest fans</h2>
<p> The people that queued your songs the most are:</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>User</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.biggest_fans %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.user__name }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Biggest fans</h2>
<p> The people that queued your songs the most are:</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>User</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.biggest_fans %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.user__name }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}

View File

@ -150,6 +150,7 @@ disable=missing-docstring,
missing-format-attribute,
too-few-public-methods,
unused-argument,
signature-differs,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

View File

@ -2,3 +2,5 @@ django>=2.2,<2.3
mutagen
argon2-cffi
prometheus_client
pylint==2.5.3
pillow