mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2025-12-09 22:32:21 +01:00
Compare commits
51 Commits
search
...
tosti-info
| Author | SHA1 | Date | |
|---|---|---|---|
| d17c6d8b18 | |||
| ef627318ba | |||
| 4491fc234b | |||
| b1a080799c | |||
| e67bc8dc5a | |||
| e5fe2aa1cf | |||
| 9529ae245a | |||
| a325ebbe82 | |||
| a422e6d4f5 | |||
| b4a6530204 | |||
| 1292694c4a | |||
| 64b26d03a1 | |||
| 2ade1a7dfa | |||
| 1a797a5d98 | |||
| b604ac9955 | |||
| bb6166c1db | |||
| 3aa876e223 | |||
| 61fa646353 | |||
| 416fb3e5a9 | |||
| 1b5b5106ba | |||
| 4a1df11b40 | |||
| f4ab85106d | |||
| 23f651bbd1 | |||
| 3724b94e4a | |||
| 4fdf25ac43 | |||
| 83406ec0ab | |||
| e447a7c210 | |||
| 91d3b0cf35 | |||
| 228d0208f2 | |||
| 62ba17ef67 | |||
| 6a549fbd7b | |||
| 371334326b | |||
| 40e2245d39 | |||
| c9ba37d291 | |||
| 6ec3d57a39 | |||
| 59993aa1c0 | |||
| a1116ca4cd | |||
| 95bd47c46a | |||
| af6031c3a5 | |||
| a3f501273f | |||
| 7af7127612 | |||
| 4c9a431f8a | |||
| 53d3c6e5c4 | |||
| 581e14f7ef | |||
| d0ed0a0a62 | |||
| ce3bfb02d5 | |||
| 065f29fe55 | |||
| 3bdac86bfb | |||
| a864e8f535 | |||
| 48dd3bd0df | |||
| 2fcd827b85 |
141
.gitignore
vendored
141
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
BIN
marietje/marietje/static/fonts/comic-serif.tff
Normal file
BIN
marietje/marietje/static/fonts/comic-serif.tff
Normal file
Binary file not shown.
@ -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']));
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
97
marietje/marietje/templates/beeldscherm.html
Normal file
97
marietje/marietje/templates/beeldscherm.html
Normal 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>
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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',)
|
||||
|
||||
|
||||
|
||||
@ -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'))},
|
||||
),
|
||||
]
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
1
pylintrc
1
pylintrc
@ -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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
setuptools
|
||||
django==2.1
|
||||
django>=2.2,<2.3
|
||||
mysqlclient
|
||||
https://projects.unbit.it/downloads/uwsgi-lts.tar.gz
|
||||
mutagen
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
django
|
||||
django>=2.2,<2.3
|
||||
mutagen
|
||||
argon2-cffi
|
||||
prometheus_client
|
||||
pylint==2.5.3
|
||||
pillow
|
||||
|
||||
Reference in New Issue
Block a user