1 Commits

Author SHA1 Message Date
b9fa1747c4 searching ignores interpunction characters 2019-04-03 17:53:06 +02:00
22 changed files with 131 additions and 601 deletions

141
.gitignore vendored
View File

@ -1,138 +1,3 @@
# Byte-compiled / optimized / DLL files /venv/
__pycache__/ *.pyc
*.py[cod] *.pyo
*$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 - apt-get -qq install -y python3 python3-venv python3-pip
- python3 -m venv venv - python3 -m venv venv
- source venv/bin/activate - source venv/bin/activate
- pip install -r requirements.txt - pip install -r requirements.txt pylint
script: script:
- pylint marietje/marietje marietje/metrics marietje/playerapi marietje/queues marietje/songs marietje/stats - pylint marietje/marietje marietje/metrics marietje/playerapi marietje/queues marietje/songs marietje/stats
@ -39,10 +39,4 @@ deploy:
"\$PYTHON" "\$MANAGE" migrate --noinput "\$PYTHON" "\$MANAGE" migrate --noinput
"\$PYTHON" "\$MANAGE" collectstatic --noinput "\$PYTHON" "\$MANAGE" collectstatic --noinput
systemctl start MarietjeDjango.service 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 EOF

View File

@ -19,5 +19,4 @@ urlpatterns = [
url(r'^volumedown', views.volume_down), url(r'^volumedown', views.volume_down),
url(r'^volumeup', views.volume_up), url(r'^volumeup', views.volume_up),
url(r'^mute', views.mute), 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 import transaction
from django.db.models import Q, Sum, Value from django.db.models import Q, Sum, Value
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import JsonResponse, HttpResponseForbidden, HttpResponse from django.http import JsonResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
@ -20,7 +20,6 @@ from prometheus_client import Counter
from marietje.utils import song_to_dict, playlist_song_to_dict, send_to_bertha from marietje.utils import song_to_dict, playlist_song_to_dict, send_to_bertha
from queues.models import PlaylistSong, QueueCommand from queues.models import PlaylistSong, QueueCommand
from songs.models import Song from songs.models import Song
from marietje.settings import MAX_MINUTES_IN_A_ROW
request_counter = Counter('marietje_requests', 'Queue requests on marietje', ['queue']) request_counter = Counter('marietje_requests', 'Queue requests on marietje', ['queue'])
upload_counter = Counter('marietje_uploads', 'Songs uploaded to marietje') upload_counter = Counter('marietje_uploads', 'Songs uploaded to marietje')
@ -95,7 +94,10 @@ def songs(request):
def search_songs(): def search_songs():
queries = [Q(deleted=False)] queries = [Q(deleted=False)]
queries.extend([Q(Q(artist__icontains=word) | Q(title__icontains=word)) for word in request.POST.get('all', '').split()])
for word in request.POST.get('all', '').split():
regexword = r"".join('[\W]?[' + letter + ']' for letter in word)
queries.extend([Q(Q(artist__iregex=regexword) | Q(title__iregex=regexword))])
queries.extend([Q(user__name__icontains=word) for word in request.POST.get('uploader', '').split()]) queries.extend([Q(user__name__icontains=word) for word in request.POST.get('uploader', '').split()])
filter_query = queries.pop() filter_query = queries.pop()
@ -169,7 +171,7 @@ def managesongs(request):
@api_auth_required @api_auth_required
def queue(request): def queue(request):
queue = request.user.queue queue = request.user.queue
infobar = {"start_personal_queue": 0, "length_personal_queue": 0, "length_total_queue": 0, "end_personal_queue": 0, 'max_length': MAX_MINUTES_IN_A_ROW} infobar = {"start_personal_queue": 0, "length_personal_queue": 0, "length_total_queue": 0, "end_personal_queue": 0}
for song in queue.queue(): for song in queue.queue():
infobar["length_total_queue"] += song.song.duration infobar["length_total_queue"] += song.song.duration
if song.user == request.user: if song.user == request.user:
@ -178,6 +180,7 @@ def queue(request):
if infobar["start_personal_queue"] == 0: if infobar["start_personal_queue"] == 0:
infobar["start_personal_queue"] = infobar["length_total_queue"] - song.song.duration infobar["start_personal_queue"] = infobar["length_total_queue"] - song.song.duration
json = { json = {
'current_song': playlist_song_to_dict(queue.current_song()), '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()], 'queue': [playlist_song_to_dict(playlist_song, user=request.user) for playlist_song in queue.queue()],
@ -189,7 +192,6 @@ def queue(request):
return JsonResponse(json) return JsonResponse(json)
@require_http_methods(["POST"])
@api_auth_required @api_auth_required
def skip(request): def skip(request):
playlist_song = request.user.queue.current_song() playlist_song = request.user.queue.current_song()
@ -349,26 +351,3 @@ def _request_weight(ps):
return float(ps.song.duration) return float(ps.song.duration)
# Count other requests for 10% # Count other requests for 10%
return 0.10 * float(ps.song.duration) 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.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator from django.contrib.auth.validators import ASCIIUsernameValidator, UnicodeUsernameValidator
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db import models from django.db import models
from django.utils import timezone from django.utils import six, timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from marietje.utils import get_first_queue from marietje.utils import get_first_queue
@ -46,7 +46,7 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin):
username_validator = UnicodeUsernameValidator() username_validator = UnicodeUsernameValidator() if six.PY3 else ASCIIUsernameValidator()
username = models.CharField( username = models.CharField(
_('username'), _('username'),

View File

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

View File

@ -183,11 +183,6 @@ function updateTime() {
} }
if (infobar['end_personal_queue'] !== 0){ if (infobar['end_personal_queue'] !== 0){
$('.start-queue').text("First song starts " + showExactOrRelative(infobar['start_personal_queue'])); $('.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() + ")"); $('.duration-queue').text( " (" + (infobar['length_personal_queue']).secondsToMMSS() + ")");
$('.end-queue').text("Last song ends " + showExactOrRelative(infobar['end_personal_queue'])); $('.end-queue').text("Last song ends " + showExactOrRelative(infobar['end_personal_queue']));
} }

View File

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

View File

@ -1,97 +0,0 @@
<!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,5 +40,4 @@ urlpatterns = [
url(r'^playerapi/', include('playerapi.urls')), url(r'^playerapi/', include('playerapi.urls')),
url(r'^stats/', include('stats.urls')), url(r'^stats/', include('stats.urls')),
url(r'^metrics', metrics, name='metrics'), url(r'^metrics', metrics, name='metrics'),
url(r'^beeldscherm/$', partial(render, template_name='beeldscherm.html'), name='beeldscherm'),
] ]

View File

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

View File

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

View File

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

View File

@ -1,7 +1,4 @@
from django.contrib import admin 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 from .models import ReportNote, Song
@ -9,40 +6,16 @@ from .models import ReportNote, Song
class ReportNoteInline(admin.StackedInline): class ReportNoteInline(admin.StackedInline):
model = ReportNote model = ReportNote
extra = 0 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) @admin.register(Song)
class SongAdmin(admin.ModelAdmin): class SongAdmin(admin.ModelAdmin):
list_display = ('artist', 'title', 'user_name', 'reports') list_display = ('artist', 'title', 'user_name', 'reports')
list_display_links = ('artist', 'title')
list_filter = (SongHasReportNoteFilter,)
search_fields = ('artist', 'title', 'user__name') search_fields = ('artist', 'title', 'user__name')
inlines = [ReportNoteInline] inlines = [ReportNoteInline]
autocomplete_fields = ('user',)
@staticmethod @staticmethod
def reports(song): def reports(song):
# num_reports is annotated by SongHasReportNoteFilter return ReportNote.objects.filter(song=song).count()
return song.num_reports
@staticmethod @staticmethod
def user_name(song): def user_name(song):
@ -57,15 +30,6 @@ class SongAdmin(admin.ModelAdmin):
@admin.register(ReportNote) @admin.register(ReportNote)
class ReportNoteAdmin(admin.ModelAdmin): class ReportNoteAdmin(admin.ModelAdmin):
exclude = ('song',)
list_display = ('song', 'note', 'user') list_display = ('song', 'note', 'user')
search_fields = ('song__artist', 'song__title', 'user__name') search_fields = ('song__artist', 'song__title', 'user__name')
autocomplete_fields = ('user',) readonly_fields = ('song',)
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 %} {% block content %}
<h1>Statistics</h1> <h1>Statistics</h1>
<div class="row display-flex"> <div class="row">
{% if not stats %} {% if not stats %}
<div class="col-xs-12 alert alert-danger"> <div class="alert alert-danger">
<strong>Stats unavailable :(</strong> <strong>Stats unavailable :(</strong>
</div> </div>
{% else %} {% else %}
{% if current_age_text %} {% if current_age_text %}
<div class="col-xs-12 alert alert-info"> <div class="alert alert-info">
<strong>{{ current_age_text }}</strong> <strong>{{ current_age_text }}</strong>
{% endif %} {% endif %}
</div> </div>
@ -72,7 +72,7 @@
<h2>Time requested</h2> <h2>Time requested</h2>
<p>In total <strong> {{ stats.total_time_requested }} </strong> of music have been requested, with an <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>. 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"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -148,34 +148,11 @@
</tbody> </tbody>
</table> </table>
</div> </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>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played uploaders</h2> <h2>Most played uploaders</h2>
<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> <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>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>

View File

@ -7,17 +7,18 @@
{% block content %} {% block content %}
<h1>User Statistics</h1> <h1>User Statistics</h1>
<div class="row display-flex"> <div class="row">
{% if not stats %} {% if not stats %}
<div class="col-xs-12 alert alert-danger"> <div class="alert alert-danger">
<strong>Stats unavailable :(</strong> <strong>Stats unavailable :(</strong>
</div> </div>
{% else %} {% else %}
{% if current_age_text %} {% if current_age_text %}
<div class="col-xs-12 alert alert-info"> <div class="alert alert-info">
<strong>{{ current_age_text }}</strong> <strong>{{ current_age_text }}</strong>
</div>
{% endif %} {% endif %}
</div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played songs</h2> <h2>Most played songs</h2>
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different <p>You have requested <strong> {{ stats.unique_requests }} </strong> different
@ -47,30 +48,7 @@
</table> </table>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="row">
<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"> <div class="col-md-6">
<h2>Uploads requested</h2> <h2>Uploads requested</h2>
<p> You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column <p> You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
@ -104,34 +82,6 @@
</table> </table>
</div> </div>
</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"> <div class="col-md-6">
<h2>Most played uploaders</h2> <h2>Most played uploaders</h2>
<p> The people whose songs you have queued the most are:</p> <p> The people whose songs you have queued the most are:</p>
@ -157,29 +107,6 @@
</table> </table>
</div> </div>
</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>
{% endif %} {% endif %}
</div> </div>

View File

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

View File

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

View File

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

View File

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