51 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
40e2245d39 Merge branch 'oslomp/artists' into 'marietje-zuid'
Artist statistics

See merge request dsprenkels/MarietjeDjango!44
2020-03-28 14:26:31 +01:00
c9ba37d291 Artist statistics 2020-03-28 14:26:31 +01:00
6ec3d57a39 Merge branch 'marietje-zuid' into 'marietje-zuid'
Remove python2 compatibility library django.utils.six

See merge request dsprenkels/MarietjeDjango!45
2020-03-28 14:09:16 +01:00
59993aa1c0 Remove unused import ASCIIUsernameValidator 2020-03-28 14:07:00 +01:00
a1116ca4cd Remove python2 compatibility library django.utils.six 2020-03-28 14:01:13 +01:00
95bd47c46a Bump django version 2020-03-04 14:56:04 +01:00
af6031c3a5 Merge branch 'revert-7af71276' into 'marietje-zuid'
Revert "Merge branch 'oslomp/search_field' into 'marietje-zuid'"

See merge request dsprenkels/MarietjeDjango!43
2019-11-01 17:34:22 +01:00
a3f501273f Revert "Merge branch 'oslomp/search_field' into 'marietje-zuid'"
This reverts merge request !42
2019-11-01 17:31:44 +01:00
7af7127612 Merge branch 'oslomp/search_field' into 'marietje-zuid'
Merged both search-fields into one

See merge request dsprenkels/MarietjeDjango!42
2019-10-31 10:32:07 +01:00
4c9a431f8a Merge both search-fields into one 2019-10-31 10:32:07 +01:00
53d3c6e5c4 Closes #11 2019-10-26 08:48:33 +02:00
581e14f7ef Merge branch 'dsprenkels/issue_15' into 'marietje-zuid'
Set all cache TTLs to 2 days

Closes #15

See merge request dsprenkels/MarietjeDjango!39
2019-10-25 19:35:59 +02:00
d0ed0a0a62 Set all cache TTLs to 2 days
Fixes #15.
2019-10-25 19:33:12 +02:00
ce3bfb02d5 Merge branch 'cron' into 'marietje-zuid'
Regenerate caches after apply patch

Closes #14

See merge request dsprenkels/MarietjeDjango!38
2019-10-25 19:20:51 +02:00
065f29fe55 gitlab-ci: Regenerate caches after apply patch
This commit fixes #14.
2019-10-25 19:18:54 +02:00
3bdac86bfb Merge branch 'new_stat' into 'marietje-zuid'
Extra statistic: biggest fans

See merge request dsprenkels/MarietjeDjango!37
2019-10-23 21:03:43 +02:00
a864e8f535 Extra statistic: biggest fans 2019-10-23 20:13:12 +02:00
48dd3bd0df Merge branch 'visual_changes' into 'marietje-zuid'
Visual changes

See merge request dsprenkels/MarietjeDjango!36
2019-04-08 18:09:16 +02:00
2fcd827b85 Visual changes 2019-04-08 18:09:14 +02:00
22 changed files with 602 additions and 129 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
@ -39,4 +39,10 @@ deploy:
"\$PYTHON" "\$MANAGE" migrate --noinput
"\$PYTHON" "\$MANAGE" collectstatic --noinput
systemctl start MarietjeDjango.service
# Regenerate caches
(
sudo -u www-data /srv/MarietjeDjango/django_env/bin/python /srv/MarietjeDjango/marietje/manage.py recache_stats
sudo -u www-data /srv/MarietjeDjango/django_env/bin/python /srv/MarietjeDjango/marietje/manage.py recache_user_stats
) &
EOF

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
@ -20,6 +20,7 @@ from prometheus_client import Counter
from marietje.utils import song_to_dict, playlist_song_to_dict, send_to_bertha
from queues.models import PlaylistSong, QueueCommand
from songs.models import Song
from marietje.settings import MAX_MINUTES_IN_A_ROW
request_counter = Counter('marietje_requests', 'Queue requests on marietje', ['queue'])
upload_counter = Counter('marietje_uploads', 'Songs uploaded to marietje')
@ -168,7 +169,7 @@ def managesongs(request):
@api_auth_required
def queue(request):
queue = request.user.queue
infobar = {"start_personal_queue": 0, "length_personal_queue": 0, "length_total_queue": 0, "end_personal_queue": 0}
infobar = {"start_personal_queue": 0, "length_personal_queue": 0, "length_total_queue": 0, "end_personal_queue": 0, 'max_length': MAX_MINUTES_IN_A_ROW}
for song in queue.queue():
infobar["length_total_queue"] += song.song.duration
if song.user == request.user:
@ -177,7 +178,6 @@ def queue(request):
if infobar["start_personal_queue"] == 0:
infobar["start_personal_queue"] = infobar["length_total_queue"] - song.song.duration
json = {
'current_song': playlist_song_to_dict(queue.current_song()),
'queue': [playlist_song_to_dict(playlist_song, user=request.user) for playlist_song in queue.queue()],
@ -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

@ -1,10 +1,10 @@
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import ASCIIUsernameValidator, UnicodeUsernameValidator
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.core.mail import send_mail
from django.db import models
from django.utils import six, timezone
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from marietje.utils import get_first_queue
@ -46,7 +46,7 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser, PermissionsMixin):
username_validator = UnicodeUsernameValidator() if six.PY3 else ASCIIUsernameValidator()
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),

View File

@ -40,8 +40,8 @@ footer {
border-bottom: 1px solid #DDDDDD;
}
.requested_song .plays-at, .requested_song .requested-by {
font-weight: bold;
tr.requested_song{
border-left: 1px solid #777777;
}
.table-header-style td, .table-header-style{
@ -49,3 +49,13 @@ footer {
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

@ -183,6 +183,11 @@ function updateTime() {
}
if (infobar['end_personal_queue'] !== 0){
$('.start-queue').text("First song starts " + showExactOrRelative(infobar['start_personal_queue']));
if (infobar['length_personal_queue'] > infobar['max_length'] * 60) {
$('.duration-queue').addClass('text-danger');
} else {
$('.duration-queue').removeClass('text-danger');
}
$('.duration-queue').text( " (" + (infobar['length_personal_queue']).secondsToMMSS() + ")");
$('.end-queue').text("Last song ends " + showExactOrRelative(infobar['end_personal_queue']));
}

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

@ -36,9 +36,11 @@
<ul class="nav navbar-nav navbar-right hidden-xs">
<li>
<div class="infobar">
<p class="navbar-text start-queue"></p>
<p class="navbar-text start-queue hidden-sm hidden-xs"></p>
<p class="navbar-text end-queue"></p>
<p class="navbar-text duration-queue"></p>
<div class="navbar-text">
<p class="duration-queue"></p>
</div>
</div>
</li>
</ul>
@ -93,7 +95,9 @@
<td class="col-md-4">Artist</td>
<td class="col-md-4">Title</td>
<td class="col-md-2 hidden-xs">Requested By</td>
<td id="timeswitch" class="col-md-1 hidden-xs text-info" >Plays In</td>
<td class="col-md-1 hidden-xs text-info" style="cursor: pointer;">
<span id="timeswitch" class="btn-link" >Plays In</span>
</td>
<td class="col-md-1 control-icons">Control</td>
</tr>
<tr class="currentsong" style="font-weight: bold">

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>
@ -72,7 +72,7 @@
<h2>Time requested</h2>
<p>In total <strong> {{ stats.total_time_requested }} </strong> of music have been requested, with an
average song length of <strong>{{ stats.total_average }}</strong>.
These are the {{ stats.stats_top_count }} people with the longest total time queued</p>
These are the {{ stats.stats_top_count }} people with the longest total time queued.</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
@ -148,11 +148,34 @@
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played Artists</h2>
<p>These are the {{ stats.stats_top_count }} most played artists ever.</p>
<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 uploaders</h2>
<p>The left column shows the {{ stats.stats_top_count }} people whose songs are requested most often by other people
people. The right column shows how many times that person has queued his own songs.</p>
<p>These are the {{ stats.stats_top_count }} people whose songs are requested most often by other people, as shown in the left column. The right column shows how many times that person has queued his own songs.</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>

View File

@ -7,107 +7,180 @@
{% 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>
</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>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="row">
<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 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 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>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>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>
{% endif %}
</div>
{% endblock %}
{% endblock %}

View File

@ -10,10 +10,12 @@ from songs.models import Song
from marietje.models import User
CACHE_TTL = 2 * 24 * 3600
def recache_stats():
new_stats = compute_stats()
caches['default'].delete('stats')
caches['default'].set('stats', new_stats, 2 * 3600)
caches['default'].set('stats', new_stats, CACHE_TTL)
return new_stats
@ -25,7 +27,7 @@ def recache_user_stats():
new_stats = user_stats(user['id'])
cacheloc = 'userstats_{}'.format(user['id'])
caches['userstats'].delete(cacheloc)
caches['userstats'].set(cacheloc, new_stats, 48 * 3600)
caches['userstats'].set(cacheloc, new_stats, CACHE_TTL)
return new_stats
@ -104,6 +106,12 @@ def compute_stats():
'song__title').annotate(total=Count('id')).order_by(
'-total', 'song__artist')[:settings.STATS_TOP_COUNT]
stats['most_played_artists'] = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'song__artist').annotate(total=Count('song__artist')).order_by(
'-total', 'song__artist')[:settings.STATS_TOP_COUNT]
stats['most_played_songs_14_days'] = PlaylistSong.objects.filter(
state=2, played_at__gte=timezone.now() -
timedelta(days=14)).exclude(user_id=None).values(
@ -163,6 +171,7 @@ def compute_stats():
'unique_request_stats': list(stats['unique_request_stats']),
'total_unique_requests': "{0:,.0f}".format(stats['total_unique_requests']['total']),
'most_played_songs': list(stats['most_played_songs']),
'most_played_artists': list(stats['most_played_artists']),
'most_played_songs_14_days': list(stats['most_played_songs_14_days']),
'time_requested': stats['time_requested'],
'total_time_requested': str(round(float(
@ -195,6 +204,12 @@ def user_stats(request):
'-total', 'song__artist',
'song__title')[:settings.STATS_TOP_COUNT]
most_played_artists = PlaylistSong.objects.filter(
user__id=request, state=2,
song_id__isnull=False).values('song__artist').annotate(
total=Count('song__artist')).order_by(
'-total', 'song__artist')[:settings.STATS_TOP_COUNT]
most_played_uploaders = PlaylistSong.objects.filter(
user__id=request, state=2).exclude(
Q(user_id=None)
@ -203,31 +218,50 @@ def user_stats(request):
total=Count('song__user__id')).order_by(
'-total')[:settings.STATS_TOP_COUNT]
biggest_fans = PlaylistSong.objects.filter(
state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(
Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'user__id', 'user__name').annotate(
total=Count('user__id')).order_by(
'-total')[:settings.STATS_TOP_COUNT]
most_played_uploads = PlaylistSong.objects.filter(
state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(
user__id=None).values('song__artist', 'song__title').annotate(
total=Count('id', filter=~Q(user__id=request)),
user_total=Count('id', filter=Q(user__id=request))).order_by(
'-total', 'song__artist',
'song__title')
'-total', 'song__artist', 'song__title')
most_played_uploaded_artists = PlaylistSong.objects.filter(
state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(
user__id=None).values('song__artist').annotate(total=Count(
'song__artist', filter=~Q(user__id=request)), user_total=Count(
'id', filter=Q(user__id=request))).order_by(
'-total', 'song__artist')
most_played = list(most_played_uploads)
total_played_uploads = 0
total_played_user_uploads = 0
total_played = {}
total_played['uploads'] = 0
total_played['user_uploads'] = 0
for x in most_played:
total_played_uploads += x['total']
total_played_user_uploads += x['user_total']
total_played['uploads'] += x['total']
total_played['user_uploads'] += x['user_total']
most_played_uploads_list = sorted(most_played_uploads, key=lambda x: (x['song__artist'], x['song__title']))
most_played_uploads_list = sorted(most_played_uploads_list, key=lambda x: x["total"], reverse=True)[:settings.STATS_TOP_COUNT]
most_played_uploaded_artists = sorted(list(most_played_uploaded_artists), key=lambda x: x["total"], reverse=True)[:settings.STATS_TOP_COUNT]
return {
'last_updated': last_updated,
'total_uploads': total_uploads,
'total_requests': total_requests,
'unique_requests': unique_requests,
'most_played_songs': list(most_played_songs),
'most_played_artists': list(most_played_artists),
'most_played_uploaders': list(most_played_uploaders),
'most_played_uploads': most_played_uploads_list,
'most_played_uploaded_artists': most_played_uploaded_artists,
'stats_top_count': settings.STATS_TOP_COUNT,
'total_played_uploads': total_played_uploads,
'total_played_user_uploads': total_played_user_uploads,
'total_played_uploads': total_played['uploads'],
'total_played_user_uploads': total_played['user_uploads'],
'biggest_fans': list(biggest_fans),
}

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

@ -1,5 +1,5 @@
setuptools
django==2.1
django>=2.2,<2.3
mysqlclient
https://projects.unbit.it/downloads/uwsgi-lts.tar.gz
mutagen

View File

@ -1,4 +1,6 @@
django
django>=2.2,<2.3
mutagen
argon2-cffi
prometheus_client
pylint==2.5.3
pillow