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
__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/
/venv/
*.pyc
*.pyo

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
- pip install -r requirements.txt pylint
script:
- pylint marietje/marietje marietje/metrics marietje/playerapi marietje/queues marietje/songs marietje/stats
@ -39,10 +39,4 @@ 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,5 +19,4 @@ 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, HttpResponse
from django.http import JsonResponse, HttpResponseForbidden
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,7 +20,6 @@ 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')
@ -95,7 +94,10 @@ def songs(request):
def search_songs():
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()])
filter_query = queries.pop()
@ -169,7 +171,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, '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():
infobar["length_total_queue"] += song.song.duration
if song.user == request.user:
@ -178,6 +180,7 @@ 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,7 +192,6 @@ def queue(request):
return JsonResponse(json)
@require_http_methods(["POST"])
@api_auth_required
def skip(request):
playlist_song = request.user.queue.current_song()
@ -349,26 +351,3 @@ 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 UnicodeUsernameValidator
from django.contrib.auth.validators import ASCIIUsernameValidator, UnicodeUsernameValidator
from django.core.mail import send_mail
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 marietje.utils import get_first_queue
@ -46,7 +46,7 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser, PermissionsMixin):
username_validator = UnicodeUsernameValidator()
username_validator = UnicodeUsernameValidator() if six.PY3 else ASCIIUsernameValidator()
username = models.CharField(
_('username'),

View File

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

View File

@ -183,11 +183,6 @@ 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,7 +37,6 @@
<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

@ -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'^stats/', include('stats.urls')),
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)
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

@ -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_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()
@ -112,7 +111,7 @@ class Queue(models.Model):
return songs[1:]
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')
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
def __str__(self):
return str(self.name)
return self.name
class QueueCommand(models.Model):
@ -162,4 +161,4 @@ class QueueCommand(models.Model):
command = models.TextField()
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">
<li>
<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>
<div class="navbar-text">
<p class="duration-queue"></p>
</div>
<p class="navbar-text duration-queue"></p>
</div>
</li>
</ul>
@ -95,9 +93,7 @@
<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 class="col-md-1 hidden-xs text-info" style="cursor: pointer;">
<span id="timeswitch" class="btn-link" >Plays In</span>
</td>
<td id="timeswitch" class="col-md-1 hidden-xs text-info" >Plays In</td>
<td class="col-md-1 control-icons">Control</td>
</tr>
<tr class="currentsong" style="font-weight: bold">

View File

@ -1,7 +1,4 @@
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
@ -9,40 +6,16 @@ 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):
# num_reports is annotated by SongHasReportNoteFilter
return song.num_reports
return ReportNote.objects.filter(song=song).count()
@staticmethod
def user_name(song):
@ -57,15 +30,6 @@ 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')
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"
readonly_fields = ('song',)

View File

@ -5,14 +5,14 @@
{% block content %}
<h1>Statistics</h1>
<div class="row display-flex">
<div class="row">
{% if not stats %}
<div class="col-xs-12 alert alert-danger">
<div class="alert alert-danger">
<strong>Stats unavailable :(</strong>
</div>
{% else %}
{% if current_age_text %}
<div class="col-xs-12 alert alert-info">
<div class="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,34 +148,11 @@
</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>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">
<table class="table table-striped">
<thead>

View File

@ -7,180 +7,107 @@
{% block content %}
<h1>User Statistics</h1>
<div class="row display-flex">
<div class="row">
{% if not stats %}
<div class="col-xs-12 alert alert-danger">
<div class="alert alert-danger">
<strong>Stats unavailable :(</strong>
</div>
{% else %}
{% if current_age_text %}
<div class="col-xs-12 alert alert-info">
<div class="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>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 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>
<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 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>
<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 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>
{% endif %}
</div>
{% endblock %}
{% endblock %}

View File

@ -10,12 +10,10 @@ 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, CACHE_TTL)
caches['default'].set('stats', new_stats, 2 * 3600)
return new_stats
@ -27,7 +25,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, CACHE_TTL)
caches['userstats'].set(cacheloc, new_stats, 48 * 3600)
return new_stats
@ -106,12 +104,6 @@ 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(
@ -171,7 +163,6 @@ 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(
@ -204,12 +195,6 @@ 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)
@ -218,50 +203,31 @@ 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')
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')
'-total', 'song__artist',
'song__title')
most_played = list(most_played_uploads)
total_played = {}
total_played['uploads'] = 0
total_played['user_uploads'] = 0
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'],
'biggest_fans': list(biggest_fans),
'total_played_uploads': total_played_uploads,
'total_played_user_uploads': total_played_user_uploads,
}

View File

@ -150,7 +150,6 @@ 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.2,<2.3
django==2.1
mysqlclient
https://projects.unbit.it/downloads/uwsgi-lts.tar.gz
mutagen

View File

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