Compare commits

..

29 Commits

Author SHA1 Message Date
34a2339b3b Bump version to 0.11.12 2023-03-03 21:56:32 -05:00
34abbe753b Fix a few display issues with charts 2023-03-03 21:55:36 -05:00
0fe00c3dd8 Fix bug in album creation 2023-03-03 21:55:26 -05:00
5a3eb7a8c8 Bump version to 0.11.11 2023-03-03 16:14:11 -05:00
e63ca13d57 Small tweaks to scorbble view 2023-03-03 16:13:06 -05:00
b3d3098fe0 Fix importing albums 2023-03-03 16:12:38 -05:00
8f5a200526 Bump version to 0.11.10 2023-03-03 12:12:19 -05:00
411d2b42b0 Add better titles to artists too 2023-03-03 11:44:36 -05:00
bce1322289 Fix images and default to Maloja 2023-03-03 11:42:56 -05:00
908819d24e Damn capital letter 2023-03-03 11:02:22 -05:00
6d21bb2e85 Bump version to 0.11.9 2023-03-03 02:41:47 -05:00
7df3fedc64 Fix bad image templates 2023-03-03 02:41:27 -05:00
b4e83b184e Bump version to 0.11.8 2023-03-03 02:32:55 -05:00
6e885df1dd Small fix to remove unused templatetag 2023-03-03 02:32:32 -05:00
f153f831b3 Bump version to 0.11.7 2023-03-03 02:29:56 -05:00
66a90c87f1 Add demo of maloja style 2023-03-03 02:29:32 -05:00
6e17e4ce0d Fix chart rank and periods 2023-03-03 02:29:15 -05:00
3c3e567573 Bump version to 0.11.6 2023-03-02 16:11:48 -05:00
2775851474 Clean up the video detail page 2023-03-02 16:11:19 -05:00
654a64e82d Stub in sports urls 2023-03-02 16:04:44 -05:00
7dd7f369d8 Fix bug in audiodb scrape path 2023-03-02 15:45:15 -05:00
fb6110c71d Bump version to 0.11.5
Version 1.0 approaches!
2023-03-02 15:11:40 -05:00
93299a1abd Hide album urls that don't exist 2023-03-02 15:08:34 -05:00
a58ddebd23 Fix typo in audiodb lookup 2023-03-02 15:08:22 -05:00
41cdb96e94 Clean up album admin with new data 2023-03-02 15:08:11 -05:00
5a8e828b81 Add rudimentary album metadata from TADB 2023-03-02 14:48:33 -05:00
c84a3072be Dont show None if bio is missing 2023-03-02 11:26:31 -05:00
0bd7ed4463 Clean up music detail views 2023-03-02 11:03:26 -05:00
ee232aa103 Fix Le Static Files 2023-03-01 19:11:16 -05:00
35 changed files with 1263 additions and 168 deletions

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "0.11.4"
version = "0.11.12"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -8,8 +8,18 @@ from scrobbles.admin import ScrobbleInline
@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = ("name", "year", "musicbrainz_id")
list_filter = ("year",)
list_display = (
"name",
"year",
"primary_artist",
"theaudiodb_genre",
"theaudiodb_mood",
"musicbrainz_id",
)
list_filter = (
"theaudiodb_score",
"theaudiodb_genre",
)
ordering = ("name",)
filter_horizontal = [
'artists',

View File

@ -0,0 +1,85 @@
# Generated by Django 4.1.5 on 2023-03-02 19:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0011_artist_thumbnail'),
]
operations = [
migrations.AddField(
model_name='album',
name='allmusic_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='discogs_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='rateyourmusic_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_description',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_genre',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_id',
field=models.CharField(
blank=True, max_length=255, null=True, unique=True
),
),
migrations.AddField(
model_name='album',
name='theaudiodb_mood',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_score',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_score_votes',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_speed',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_style',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_theme',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='wikidata_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='wikipedia_slug',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-03-02 19:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0012_album_allmusic_id_album_discogs_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='album',
name='theaudiodb_score',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-03-02 19:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0013_alter_album_theaudiodb_score'),
]
operations = [
migrations.AddField(
model_name='album',
name='theaudiodb_year_released',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from scrobbles.mixins import ScrobblableMixin
from scrobbles.theaudiodb import lookup_artist_from_tadb
from vrobbler.apps.scrobbles.theaudiodb import lookup_album_from_tadb
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
@ -86,6 +87,21 @@ class Album(TimeStampedModel):
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
cover_image = models.ImageField(upload_to="albums/", **BNULL)
theaudiodb_id = models.CharField(max_length=255, unique=True, **BNULL)
theaudiodb_description = models.TextField(**BNULL)
theaudiodb_year_released = models.IntegerField(**BNULL)
theaudiodb_score = models.FloatField(**BNULL)
theaudiodb_score_votes = models.IntegerField(**BNULL)
theaudiodb_genre = models.CharField(max_length=255, **BNULL)
theaudiodb_style = models.CharField(max_length=255, **BNULL)
theaudiodb_mood = models.CharField(max_length=255, **BNULL)
theaudiodb_speed = models.CharField(max_length=255, **BNULL)
theaudiodb_theme = models.CharField(max_length=255, **BNULL)
allmusic_id = models.CharField(max_length=255, **BNULL)
rateyourmusic_id = models.CharField(max_length=255, **BNULL)
wikipedia_slug = models.CharField(max_length=255, **BNULL)
discogs_id = models.CharField(max_length=255, **BNULL)
wikidata_id = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.name
@ -112,6 +128,17 @@ class Album(TimeStampedModel):
def primary_artist(self):
return self.artists.first()
def scrape_theaudiodb(self) -> None:
artist = "Various Artists"
if self.primary_artist:
artist = self.primary_artist.name
album_data = lookup_album_from_tadb(self.name, artist)
if not album_data.get('theaudiodb_id'):
logger.info(f"No data for {self} found in TheAudioDB")
return
Album.objects.filter(pk=self.pk).update(**album_data)
def fix_metadata(self):
if (
not self.musicbrainz_albumartist_id
@ -159,6 +186,7 @@ class Album(TimeStampedModel):
or self.cover_image == 'default-image-replace-me'
):
self.fetch_artwork()
self.scrape_theaudiodb()
def fetch_artwork(self, force=False):
if not self.cover_image and not force:
@ -197,9 +225,27 @@ class Album(TimeStampedModel):
self.save()
@property
def mb_link(self):
def mb_link(self) -> str:
return f"https://musicbrainz.org/release/{self.musicbrainz_id}"
@property
def allmusic_link(self) -> str:
if self.allmusic_id:
return f"https://www.allmusic.com/artist/{self.allmusic_id}"
return ""
@property
def wikipedia_link(self):
if self.wikipedia_slug:
return f"https://www.wikipedia.org/en/{self.wikipedia_slug}"
return ""
@property
def tadb_link(self):
if self.theaudiodb_id:
return f"https://www.theaudiodb.com/album/{self.theaudiodb_id}"
return ""
class Track(ScrobblableMixin):
COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)

View File

@ -43,38 +43,27 @@ def get_or_create_artist(name: str, mbid: str = None) -> Artist:
def get_or_create_album(name: str, artist: Artist, mbid: str = None) -> Album:
album = None
album_created = False
albums = Album.objects.filter(name__iexact=name)
if albums.count() == 1:
album = albums.first()
else:
for potential_album in albums:
if artist in album.artist_set.all():
album = potential_album
if not album:
album_created = True
album = Album.objects.create(name=name, musicbrainz_id=mbid)
album.save()
album.artists.add(artist)
album_dict = lookup_album_dict_from_mb(name, artist_name=artist.name)
mbid = mbid or album_dict['mb_id']
if album_created or not mbid:
album_dict = lookup_album_dict_from_mb(
album.name, artist_name=artist.name
)
logger.debug(f'Looking up album {name} and mbid: {mbid}')
album = Album.objects.filter(musicbrainz_id=mbid).first()
if not album:
album = Album.objects.create(name=name, musicbrainz_id=mbid)
album.year = album_dict["year"]
album.musicbrainz_id = album_dict["mb_id"]
album.musicbrainz_releasegroup_id = album_dict["mb_group_id"]
album.musicbrainz_albumartist_id = artist.musicbrainz_id
album.save(
update_fields=[
"year",
"musicbrainz_id",
"musicbrainz_releasegroup_id",
"musicbrainz_albumartist_id",
]
)
album.artists.add(artist)
album.fetch_artwork()
return album

View File

@ -1,5 +1,4 @@
from datetime import timedelta
from django.utils import timezone
from django.db.models import Count
from django.views import generic
from music.models import Album, Artist, Track
from scrobbles.models import ChartRecord
@ -8,6 +7,7 @@ from scrobbles.stats import get_scrobble_count_qs
class TrackListView(generic.ListView):
model = Track
paginate_by = 200
def get_queryset(self):
return get_scrobble_count_qs(user=self.request.user).order_by(
@ -21,7 +21,6 @@ class TrackDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
context_data['charts'] = ChartRecord.objects.filter(
track=self.object, rank__in=[1, 2, 3]
)
@ -30,9 +29,22 @@ class TrackDetailView(generic.DetailView):
class ArtistListView(generic.ListView):
model = Artist
paginate_by = 100
def get_queryset(self):
return super().get_queryset().order_by("name")
return (
super()
.get_queryset()
.annotate(scrobble_count=Count('track__scrobble'))
.order_by("-scrobble_count")
)
def get_context_data(self, *, object_list=None, **kwargs):
context_data = super().get_context_data(
object_list=object_list, **kwargs
)
context_data['view'] = self.request.GET.get('view')
return context_data
class ArtistDetailView(generic.DetailView):
@ -41,6 +53,17 @@ class ArtistDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
artist = context_data['object']
rank = 1
tracks_ranked = []
scrobbles = artist.tracks.first().scrobble_count
for track in artist.tracks:
if scrobbles > track.scrobble_count:
rank += 1
tracks_ranked.append((rank, track))
scrobbles = track.scrobble_count
context_data['tracks_ranked'] = tracks_ranked
context_data['charts'] = ChartRecord.objects.filter(
artist=self.object, rank__in=[1, 2, 3]
)
@ -50,6 +73,14 @@ class ArtistDetailView(generic.DetailView):
class AlbumListView(generic.ListView):
model = Album
def get_queryset(self):
return (
super()
.get_queryset()
.annotate(scrobble_count=Count('track__scrobble'))
.order_by("-scrobble_count")
)
class AlbumDetailView(generic.DetailView):
model = Album

View File

@ -1,23 +1,18 @@
import datetime
import pytz
from django.conf import settings
from django.utils import timezone
import calendar
from datetime import datetime, timedelta
# need to translate to a non-naive timezone, even if timezone == settings.TIME_ZONE, so we can compare two dates
def to_user_timezone(date, profile):
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
return date.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)).astimezone(
pytz.timezone(timezone)
)
return date.astimezone(pytz.timezone(timezone))
def to_system_timezone(date, profile):
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
return date.replace(tzinfo=pytz.timezone(timezone)).astimezone(
pytz.timezone(settings.TIME_ZONE)
)
def to_system_timezone(date):
return date.astimezone(pytz.timezone(settings.TIME_ZONE))
def now_user_timezone(profile):
@ -25,9 +20,39 @@ def now_user_timezone(profile):
return timezone.localtime(timezone.now())
def now_system_timezone():
return (
datetime.datetime.now()
.replace(tzinfo=pytz.timezone(settings.TIME_ZONE))
.astimezone(pytz.timezone(settings.TIME_ZONE))
)
def start_of_day(dt, profile) -> datetime:
"""Get the start of the day in the profile's timezone"""
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
tzinfo = pytz.timezone(timezone)
return datetime.combine(dt, datetime.min.time(), tzinfo)
def end_of_day(dt, profile) -> datetime:
"""Get the start of the day in the profile's timezone"""
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
tzinfo = pytz.timezone(timezone)
return datetime.combine(dt, datetime.max.time(), tzinfo)
def start_of_week(dt, profile) -> datetime:
# TODO allow profile to set start of week
return start_of_day(dt, profile) - timedelta(dt.weekday())
def end_of_week(dt, profile) -> datetime:
# TODO allow profile to set start of week
return start_of_week(dt, profile) + timedelta(days=6)
def start_of_month(dt, profile) -> datetime:
return start_of_day(dt, profile).replace(day=1)
def end_of_month(dt, profile) -> datetime:
next_month = end_of_day(dt, profile).replace(day=28) + timedelta(days=4)
# subtracting the number of the current day brings us back one month
return next_month - timedelta(days=next_month.day)
def start_of_year(dt, profile) -> datetime:
return start_of_day(dt, profile).replace(month=1, day=1)

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.5 on 2023-03-03 00:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scrobbles', '0023_alter_audioscrobblertsvimport_options_and_more'),
]
operations = [
migrations.AddField(
model_name='chartrecord',
name='period_end',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='chartrecord',
name='period_start',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -1,4 +1,5 @@
import calendar
import datetime
import logging
from uuid import uuid4
@ -15,6 +16,14 @@ from scrobbles.utils import check_scrobble_for_finish
from sports.models import SportEvent
from videos.models import Series, Video
from vrobbler.apps.profiles.utils import (
end_of_day,
end_of_month,
end_of_week,
start_of_day,
start_of_month,
start_of_week,
)
from vrobbler.apps.scrobbles.stats import build_charts
logger = logging.getLogger(__name__)
@ -62,6 +71,7 @@ class BaseFileImportMixin(TimeStampedModel):
from scrobbles.models import Scrobble
if not self.process_log:
logger.warning("No lines in process log found to undo")
return
@ -256,6 +266,26 @@ class ChartRecord(TimeStampedModel):
series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING, **BNULL)
track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
period_start = models.DateTimeField(**BNULL)
period_end = models.DateTimeField(**BNULL)
def save(self, *args, **kwargs):
profile = self.user.profile
if self.week:
# set start and end to start and end of week
period = datetime.date.fromisocalendar(self.year, self.week, 1)
self.period_start = start_of_week(period, profile)
self.period_start = end_of_week(period, profile)
if self.day:
period = datetime.datetime(self.year, self.month, self.day)
self.period_start = start_of_day(period, profile)
self.period_end = end_of_day(period, profile)
if self.month and not self.day:
period = datetime.datetime(self.year, self.month, 1)
self.period_start = start_of_month(period, profile)
self.period_end = end_of_month(period, profile)
super(ChartRecord, self).save(*args, **kwargs)
@property
def media_obj(self):

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -6,8 +6,11 @@ from typing import Optional
import pytz
from django.apps import apps
from django.conf import settings
from django.db.models import Count, Q, ExpressionWrapper, OuterRef, Subquery
from django.contrib.auth import get_user_model
from django.db.models import Count, Q
from django.utils import timezone
User = get_user_model()
logger = logging.getLogger(__name__)
@ -102,11 +105,11 @@ def get_scrobble_count_qs(
def build_charts(
user: "User",
year: Optional[int] = None,
month: Optional[int] = None,
week: Optional[int] = None,
day: Optional[int] = None,
user=None,
model_str="Track",
):
ChartRecord = apps.get_model(
@ -140,3 +143,94 @@ def build_charts(
ChartRecord.objects.bulk_create(
chart_records, ignore_conflicts=True, batch_size=500
)
def build_yesterdays_charts_for_user(user: "User", model_str="Track") -> None:
"""Given a user calculate needed charts."""
ChartRecord = apps.get_model(
app_label='scrobbles', model_name='ChartRecord'
)
tz = pytz.timezone(settings.TIME_ZONE)
if user and user.is_authenticated:
tz = pytz.timezone(user.profile.timezone)
now = timezone.now().astimezone(tz)
yesterday = now - timedelta(days=1)
logger.info(
f"Generating charts for yesterday ({yesterday.date()}) for {user}"
)
# Always build yesterday's chart
ChartRecord.build(
user,
year=yesterday.year,
month=yesterday.month,
day=yesterday.day,
model_str=model_str,
)
now_week = now.isocalendar()[1]
yesterday_week = now.isocalendar()[1]
if now_week != yesterday_week:
logger.info(
f"New weekly charts for {yesterday.year}-{yesterday_week} for {user}"
)
ChartRecord.build(
user,
year=yesterday.year,
month=yesterday_week,
model_str=model_str,
)
# If the month has changed, build charts
if now.month != yesterday.month:
logger.info(
f"New monthly charts for {yesterday.year}-{yesterday.month} for {user}"
)
ChartRecord.build(
user,
year=yesterday.year,
month=yesterday.month,
model_str=model_str,
)
# If the year has changed, build charts
if now.year != yesterday.year:
logger.info(f"New annual charts for {yesterday.year} for {user}")
ChartRecord.build(user, year=yesterday.year, model_str=model_str)
def build_missing_charts_for_user(user: "User", model_str="Track") -> None:
""""""
ChartRecord = apps.get_model(
app_label='scrobbles', model_name='ChartRecord'
)
Scrobble = apps.get_model(app_label='scrobbles', model_name='Scrobble')
logger.info(f"Generating historical charts for {user}")
tz = pytz.timezone(settings.TIME_ZONE)
if user and user.is_authenticated:
tz = pytz.timezone(user.profile.timezone)
now = timezone.now().astimezone(tz)
first_scrobble = (
Scrobble.objects.filter(user=user, played_to_completion=True)
.order_by('created')
.first()
)
start_date = first_scrobble.timestamp
days_since = (now - start_date).days
for day_num in range(0, days_since):
build_date = start_date + timedelta(days=day_num)
logger.info(f"Generating chart batch for {build_date}")
ChartRecord.build(user=user, year=build_date.year)
ChartRecord.build(
user=user, year=build_date.year, week=build_date.isocalendar()[1]
)
ChartRecord.build(
user=user, year=build_date.year, month=build_date.month
)
ChartRecord.build(
user=user,
year=build_date.year,
month=build_date.month,
day=build_date.day,
)

View File

@ -1,13 +1,17 @@
import logging
from celery import shared_task
from celery import shared_task
from django.contrib.auth import get_user_model
from scrobbles.models import (
AudioScrobblerTSVImport,
KoReaderImport,
LastFmImport,
)
from vrobbler.apps.scrobbles.stats import build_yesterdays_charts_for_user
logger = logging.getLogger(__name__)
User = get_user_model()
@shared_task
@ -35,3 +39,9 @@ def process_koreader_import(import_id):
logger.warn(f"KOReaderImport not found with id {import_id}")
koreader_import.process()
@shared_task
def create_yesterdays_charts():
for user in User.objects.all():
build_yesterdays_charts_for_user(user)

View File

@ -0,0 +1,10 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def urlreplace(context, **kwargs):
query = context['request'].GET.copy()
query.update(kwargs)
return query.urlencode()

View File

@ -5,14 +5,15 @@ import requests
from django.conf import settings
THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY")
SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
ARTIST_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
ALBUM_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/searchalbum.php?s="
logger = logging.getLogger(__name__)
def lookup_artist_from_tadb(name: str) -> dict:
artist_info = {}
response = requests.get(SEARCH_URL + name)
response = requests.get(ARTIST_SEARCH_URL + name)
if response.status_code != 200:
logger.warn(f"Bad response from TADB: {response.status_code}")
@ -26,9 +27,52 @@ def lookup_artist_from_tadb(name: str) -> dict:
if results['artists']:
artist = results['artists'][0]
artist_info['biography'] = artist['strBiographyEN']
artist_info['genre'] = artist['strGenre']
artist_info['mood'] = artist['strMood']
artist_info['thumb_url'] = artist['strArtistThumb']
artist_info['biography'] = artist.get('strBiographyEN')
artist_info['genre'] = artist.get('strGenre')
artist_info['mood'] = artist.get('strMood')
artist_info['thumb_url'] = artist.get('strArtistThumb')
return artist_info
def lookup_album_from_tadb(name: str, artist: str) -> dict:
album_info = {}
response = requests.get(''.join([ALBUM_SEARCH_URL, artist, "&a=", name]))
if response.status_code != 200:
logger.warn(f"Bad response from TADB: {response.status_code}")
return {}
if not response.content:
logger.warn(f"Bad content from TADB: {response.content}")
return {}
results = json.loads(response.content)
if results['album']:
album = results['album'][0]
album_info['theaudiodb_id'] = album.get('idAlbum')
album_info['theaudiodb_description'] = album.get('strDescriptionEN')
album_info['theaudiodb_genre'] = album.get('strGenre')
album_info['theaudiodb_style'] = album.get('strStyle')
album_info['theaudiodb_mood'] = album.get('strMood')
album_info['theaudiodb_speed'] = album.get('strSpeed')
album_info['theaudiodb_theme'] = album.get('strTheme')
album_info['allmusic_id'] = album.get('strAllMusicID')
album_info['wikipedia_slug'] = album.get('strWikipediaID')
album_info['discogs_id'] = album.get('strDiscogsID')
album_info['wikidata_id'] = album.get('strWikidataID')
album_info['rateyourmusic_id'] = album.get('strRateYourMusicID')
if album.get('intYearReleased'):
album_info['theaudiodb_year_released'] = float(
album.get('intYearReleased')
)
if album.get('intScore'):
album_info['theaudiodb_score'] = float(album.get('intScore'))
if album.get('intScoreVotes'):
album_info['theaudiodb_score_votes'] = int(
album.get('intScoreVotes')
)
return album_info

View File

@ -69,7 +69,6 @@ class RecentScrobbleList(ListView):
data = super().get_context_data(**kwargs)
user = self.request.user
if user.is_authenticated:
completed_for_user = Scrobble.objects.filter(
played_to_completion=True, user=user
)
@ -84,14 +83,34 @@ class RecentScrobbleList(ListView):
data['sport_scrobble_list'] = completed_for_user.filter(
sport_event__isnull=False
).order_by('-timestamp')[:15]
data['active_imports'] = AudioScrobblerTSVImport.objects.filter(
processing_started__isnull=False,
processed_finished__isnull=True,
user=self.request.user,
)
data["weekly_data"] = week_of_scrobbles(user=user)
limit = 14
artist = {'user': user, 'media_type': 'Artist', 'limit': limit}
# This is weird. They don't display properly as QuerySets, so we cast to lists
data['current_artist_charts'] = {
"today": list(live_charts(**artist, chart_period="today")),
"week": list(live_charts(**artist, chart_period="week")),
"month": list(live_charts(**artist, chart_period="month")),
"year": list(live_charts(**artist, chart_period="year")),
"all": list(live_charts(**artist)),
}
track = {'user': user, 'media_type': 'Track', 'limit': limit}
data['current_track_charts'] = {
"today": list(live_charts(**track, chart_period="today")),
"week": list(live_charts(**track, chart_period="week")),
"month": list(live_charts(**track, chart_period="month")),
"year": list(live_charts(**track, chart_period="year")),
"all": list(live_charts(**track)),
}
data["weekly_data"] = week_of_scrobbles(user=user)
data['counts'] = scrobble_counts(user)
data['imdb_form'] = ScrobbleForm
data['export_form'] = ExportScrobbleForm
@ -457,20 +476,39 @@ class ChartRecordView(TemplateView):
context_data["artist_charts"] = {}
if not date:
limit = 20
artist_params = {'user': user, 'media_type': 'Artist'}
context_data['current_artist_charts'] = {
"today": live_charts(**artist_params, chart_period="today"),
"week": live_charts(**artist_params, chart_period="week"),
"month": live_charts(**artist_params, chart_period="month"),
"all": live_charts(**artist_params),
"today": live_charts(
**artist_params, chart_period="today", limit=limit
),
"week": live_charts(
**artist_params, chart_period="week", limit=limit
),
"month": live_charts(
**artist_params, chart_period="month", limit=limit
),
"year": live_charts(
**artist_params, chart_period="year", limit=limit
),
"all": live_charts(**artist_params, limit=limit),
}
track_params = {'user': user, 'media_type': 'Track'}
context_data['current_track_charts'] = {
"today": live_charts(**track_params, chart_period="today"),
"week": live_charts(**track_params, chart_period="week"),
"month": live_charts(**track_params, chart_period="month"),
"all": live_charts(**track_params),
"today": live_charts(
**track_params, chart_period="today", limit=limit
),
"week": live_charts(
**track_params, chart_period="week", limit=limit
),
"month": live_charts(
**track_params, chart_period="month", limit=limit
),
"year": live_charts(
**track_params, chart_period="year", limit=limit
),
"all": live_charts(**track_params, limit=limit),
}
return context_data

View File

@ -0,0 +1,18 @@
from django.urls import path
from sports import views
app_name = 'sports'
urlpatterns = [
path(
'sport-events/',
views.SportEventListView.as_view(),
name='event_list',
),
path(
'sport-events/<slug:slug>/',
views.SportEventDetailView.as_view(),
name='event_detail',
),
]

View File

@ -0,0 +1,12 @@
from django.views import generic
from sports.models import SportEvent
class SportEventListView(generic.ListView):
model = SportEvent
paginate_by = 50
class SportEventDetailView(generic.DetailView):
model = SportEvent
slug_field = 'uuid'

View File

@ -231,11 +231,10 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "static/"
STATIC_URL = "/static/"
STATIC_ROOT = os.getenv(
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
)
MEDIA_URL = "/media/"
MEDIA_ROOT = os.getenv(
"VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media")

View File

@ -10,6 +10,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
@ -211,6 +212,7 @@
Dashboard
</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/charts/">
<span data-feather="bar-chart"></span>
@ -229,6 +231,12 @@
Artists
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/albums/">
<span data-feather="music"></span>
Albums
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/movies/">
<span data-feather="film"></span>
@ -241,7 +249,6 @@
TV Shows
</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="/admin/">
<span data-feather="key"></span>
@ -260,13 +267,13 @@
{% for scrobble in now_playing_list %}
<div>
{% if scrobble.media_obj.album.cover_image %}
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120
style="border:1px solid black; " /></td><br/>
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120 style="border:1px solid black; " /></td><br/>
{% endif %}
{{scrobble.media_obj.title}}<br/>
{% if scrobble.media_obj.subtitle %}<em>{{scrobble.media_obj.subtitle}}</em><br/>{% endif %}
<small>{{scrobble.timestamp|naturaltime}}<br/>
from {{scrobble.source}}</small>
<a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a><br/>
{% if scrobble.media_obj.subtitle %}
<em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em><br/>
{% endif %}
<small>{{scrobble.timestamp|naturaltime}}<br/> from {{scrobble.source}}</small>
<div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
@ -295,54 +302,6 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script><!-- comment ------------------------------------------------->
/* globals Chart:false, feather:false */
(function () {
'use strict'
feather.replace({ 'aria-hidden': 'true' })
// Graphs
var ctx = document.getElementById('myChart')
// eslint-disable-next-line no-unused-vars
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: [
{% for day in weekly_data.keys %}
"{{day}}"{% if not forloop.last %},{% endif %}
{% endfor %}
],
datasets: [{
data: [
{% for count in weekly_data.values %}
{{count}}{% if not forloop.last %},{% endif %}
{% endfor %}
],
lineTension: 0,
backgroundColor: 'transparent',
borderColor: '#007bf0',
borderWidth: 4,
pointBackgroundColor: '#007bff'
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: false
}
}]
},
legend: {
display: false
}
}
})
})()
</script>
{% block extra_js %}
{% endblock %}
</body>

View File

@ -12,6 +12,13 @@
<img src="{{object.cover_image.url}}" width=300 height=300 />
</p>
{% endif %}
<div style="float:left; width:600px; margin-left:10px; ">
{% if object.theaudiodb_description %}
<p>{{object.theaudiodb_description|safe|linebreaks|truncatewords:160}}</p>
<hr/>
{% endif %}
<p><a href="{{album.mb_link}}">Musicbrainz</a> {% if album.tadb_link %}| <a href="{{album.tadb_link}}">TheAudioDB</a>{% endif %} {% if album.allmusic_link %}| <a href="{{album.allmusic_link}}">AllMusic</a>{% endif %}</p>
</div>
</div>
<div class="row">
<p>{{object.scrobbles.count}} scrobbles</p>

View File

@ -1,11 +1,82 @@
{% extends "base_list.html" %}
{% load urlreplace %}
{% block title %}Albums{% endblock %}
{% block lists %}
{% for album in object_list %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{album.cover_image.url}}" width=120 height=120 /></dd>
</dl>
{% endfor %}
<div class="row">
<p class="view">
<span class="view-links">
{% if view == 'grid' %}
View as <a href="?{% urlreplace view='list' %}">List</a>
{% else %}
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
{% endif %}
</span>
</p>
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</p>
<hr />
{% if view == 'grid' %}
<div>
{% for album in object_list %}
{% if album.cover_image %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{album.cover_image.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Scrobbles</th>
<th scope="col">Album</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for album in object_list %}
<tr>
<td>{{album.scrobbles.count}}</td>
<td><a href="{{album.get_absolute_url}}">{{album}}</a></td>
<td><a href="{{album.primary_artist.get_absolute_url}}">{{album.primary_artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -4,12 +4,11 @@
{% block title %}{{object.name}}{% endblock %}
{% block lists %}
<div class="row">
{% if object.thumbnail %}
<p style="float:left; width:302px; padding:0; border: 1px solid #ccc">
<img src="{{artist.thumbnail.url}}" width=300 height=300 />
<p style="float:left; width:300px; margin-right:10px;">
<img style="border:1px solid #ccc;" src="{{artist.thumbnail.url}}" width=300 height=300 />
</p>
{% else %}
{% if object.album_set.first.cover_image %}
@ -18,6 +17,13 @@
</p>
{% endif %}
{% endif %}
<div style="float:left; width:600px; margin-left:10px; ">
{% if artist.biography %}
<p>{{artist.biography|safe|linebreaks|truncatewords:160}}</p>
<hr/>
{% endif %}
<p><a href="{{artist.mb_link}}">Musicbrainz</a></p>
</div>
</div>
<div class="row">
<p>{{artist.scrobbles.count}} scrobbles</p>
@ -38,15 +44,15 @@
</tr>
</thead>
<tbody>
{% for track in object.tracks %}
{% for track in tracks_ranked %}
<tr>
<td>{{rank}}#1</td>
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
<td><a href="{{track.album.get_absolute_url}}">{{track.album}}</a></td>
<td>{{track.scrobble_count}}</td>
<td>#{{track.0}}</td>
<td><a href="{{track.1.get_absolute_url}}">{{track.1.title}}</a></td>
<td><a href="{{track.1.album.get_absolute_url}}">{{track.1.album}}</a></td>
<td>{{track.1.scrobble_count}}</td>
<td>
<div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{track.scrobble_count|mul:10}}%;"></span>
<span class="progress-bar-fill" style="width: {{track.1.scrobble_count|mul:10}}%;"></span>
</div>
</td>
</tr>

View File

@ -1,30 +1,79 @@
{% extends "base_list.html" %}
{% load urlreplace %}
{% block title %}Artists{% endblock %}
{% block lists %}
<div class="row">
<p class="view">
<span class="view-links">
{% if view == 'grid' %}
View as <a href="?{% urlreplace view='list' %}">List</a>
{% else %}
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
{% endif %}
</span>
</p>
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</p>
<hr />
{% if view == 'grid' %}
<div>
{% for artist in object_list %}
{% if artist.thumbnail %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{artist.thumbnail.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Artist</th>
<th scope="col">Scrobbles</th>
<th scope="col">All time</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for artist in object_list %}
<tr>
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
<td>{{artist.scrobbles.count}}</td>
<td></td>
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -3,10 +3,57 @@
{% block title %}Tracks{% endblock %}
{% block lists %}
<h2>All time</h2>
{% for track in object_list %}
<ul>
<li><a href="{{track.get_absolute_url}}">{{track}}</a></li>
</ul>
{% endfor %}
<div class="row">
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</p>
<hr />
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Scrobbles</th>
<th scope="col">Track</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for track in object_list %}
<tr>
<td>{{track.scrobble_set.count}}</td>
<td><a href="{{track.get_absolute_url}}">{{track}}</a></td>
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -67,7 +67,7 @@
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="artist-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
{% if chart_name == "all" %}All Time{% else %}{% if chart_name != "today" %}This {% endif %}{{chart_name|capfirst}}{% endif %}
</button>
</li>
{% endfor %}
@ -110,7 +110,7 @@
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="track-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#track-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
{% if chart_name == "all" %}All Time{% else %}{% if chart_name != "today" %}This {% endif %}{{chart_name|capfirst}}{% endif %}
</button>
</li>
{% endfor %}

View File

@ -1,7 +1,49 @@
{% extends "base.html" %}
{% load humanize %}
{% load static %}
{% block head_extra %}
<style>
.container { margin-bottom:100px; }
h2 { padding-top:20px; }
.image-wrapper {
contain: content;
}
.image-wrapper :hover {
background:rgba(0,0,0,0.3);
}
.caption {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 90%;
color:white;
background:rgba(0,0,0,0.4);
}
.caption-medium {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 75%;
color:white;
background:rgba(0,0,0,0.4);
}
.caption-small {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 60%;
color:white;
background:rgba(0,0,0,0.4);
}
</style>
{% endblock %}
{% block content %}
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div
@ -25,7 +67,7 @@
<div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span data-feather="calendar"></span>
<div data-feather="calendar"></div>
This week
</button>
<div class="dropdown-menu" data-bs-toggle="#graphDataChange" aria-labelledby="graphDateButton">
@ -36,14 +78,306 @@
</div>
</div>
<canvas class="my-4 w-100" id="myChart" width="900" height="220"></canvas>
<canvas class="my-4 w-100" id="myChart" width="900" height="150"></canvas>
<div class="container">
{% if user.is_authenticated %}
<div class="row">
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> |
This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
<h2>Top Artist</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for chart_name in current_artist_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.counter == 2 %}active{% endif %}"
id="artist-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{% if chart_name == "all" %}All Time{% else %}{% if chart_name != "today" %}This {% endif %}{{chart_name|capfirst}}{% endif %}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="artistTabContent" class="maloja-chart">
{% for chart_name, artists in current_artist_charts.items %}
<div class="tab-pane fade {% if forloop.counter == 2 %}show active{% endif %}" id="artist-{{chart_name}}" role="tabpanel" aria-labelledby="artist-{{chart_name}}-tab">
<div style="display:block">
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">#1 {{artists.0.name}}</div>
{% if artists.0.thumbnail %}
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{{artists.0.thumbnail.url}}" width="300px"></a>
{% else %}
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{% static "images/artist-placeholder.jpg" %}" width="300px"></a>
{% endif %}
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#2 {{artists.1.name}}</div>
{% if artists.1.thumbnail %}
<a href="{{artists.1.get_absolute_url}}"><img lt="{{artists.1.name}}" src="{{artists.1.thumbnail.url}}" width="150px"></a>
{% else %}
<a href="{{artists.1.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#3 {{artists.2.name}}</div>
{% if artists.2.thumbnail %}
<a href="{{artists.2.get_absolute_url}}"><img src="{{artists.2.thumbnail.url}}" width="150px"></a>
{% else %}
<a href="{{artists.2.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#4 {{artists.3.name}}</div>
{% if artists.3.thumbnail %}
<a href="{{artists.3.get_absolute_url}}"><img src="{{artists.3.thumbnail.url}}" width="150px"></a>
{% else %}
<a href="{{artists.3.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#5 {{artists.4.name}}</div>
{% if artists.4.thumbnail %}
<a href="{{artists.4.get_absolute_url}}"><img src="{{artists.4.thumbnail.url}}" width="150px"></a>
{% else %}
<a href="{{artists.4.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="150px"></a>
{% endif %}
</div>
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#6 {{artists.5.name}}</div>
{% if artists.5.thumbnail %}
<a href="{{artists.5.get_absolute_url}}"><img src="{{artists.5.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.5.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#7 {{artists.6.name}}</div>
{% if artists.6.thumbnail %}
<a href="{{artists.6.get_absolute_url}}"><img src="{{artists.6.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.6.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#8 {{artists.7.name}}</div>
{% if artists.7.thumbnail %}
<a href="{{artists.7.get_absolute_url}}"><img src="{{artists.7.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.7.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#9 {{artists.8.name}}</div>
{% if artists.8.thumbnail %}
<a href="{{artists.8.get_absolute_url}}"><img src="{{artists.8.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.8.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#10 {{artists.9.name}}</div>
{% if artists.9.thumbnail %}
<a href="{{artists.9.get_absolute_url}}"><img src="{{artists.9.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.9.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#11 {{artists.10.name}}</div>
{% if artists.10.thumbnail %}
<a href="{{artists.10.get_absolute_url}}"><img src="{{artists.10.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.10.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#12 {{artists.11.name}}</div>
{% if artists.11.thumbnail %}
<a href="{{artists.11.get_absolute_url}}"><img src="{{artists.11.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.11.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#13 {{artists.12.name}}</div>
{% if artists.12.thumbnail %}
<a href="{{artists.12.get_absolute_url}}"><img src="{{artists.12.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.12.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#14 {{artists.13.name}}</div>
{% if artists.13.thumbnail %}
<a href="{{artists.13.get_absolute_url}}"><img src="{{artists.13.thumbnail.url}}" width="100px"></a>
{% else %}
<a href="{{artists.13.get_absolute_url}}"><img src="{% static "images/artist-placeholder.jpg" %}" width="100px"></a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row">
<h2>Top Tracks</h2>
<ul class="nav nav-tabs" id="trackTab" role="tablist">
{% for chart_name in current_track_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.counter == 2 %}active{% endif %}" id="track-{{chart_name}}-tab" data-bs-toggle="tab"
data-bs-target="#track-{{chart_name}}" type="button" role="tab" aria-controls="home" aria-selected="true">
{% if chart_name == "all" %}All Time{% else %}{% if chart_name != "today" %}This {% endif %}{{chart_name|capfirst}}{% endif %}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="trackTabContent" class="maloja-chart">
{% for chart_name, tracks in current_track_charts.items %}
<div class="tab-pane fade {% if forloop.counter == 2 %}show active{% endif %}" id="track-{{chart_name}}" role="tabpanel" aria-labelledby="track-{{chart_name}}-tab">
<div style="display:block">
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">#1 {{tracks.0.title}}</div>
{% if tracks.0.album.cover_image %}
<a href="{{tracks.0.get_absolute_url}}"><img src="{{tracks.0.album.cover_image.url}}" width="300px"></a>
{% else %}
<a href="{{tracks.0.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="300px"></a>
{% endif %}
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">#2 {{tracks.1.title}}</div>
{% if tracks.1.album.cover_image %}
<a href="{{tracks.1.get_absolute_url}}"><img src="{{tracks.1.album.cover_image.url}}" width="150px"></a>
{% else %}
<a href="{{tracks.1.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">#3 {{tracks.2.title}}</div>
{% if tracks.2.album.cover_image %}
<a href="{{tracks.2.get_absolute_url}}"><img src="{{tracks.2.album.cover_image.url}}" width="150px"></a>
{% else %}
<a href="{{tracks.2.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">#4 {{tracks.3.title}}</div>
{% if tracks.3.album.cover_image %}
<a href="{{tracks.3.get_absolute_url}}"><img src="{{tracks.3.album.cover_image.url}}" width="150px"></a>
{% else %}
<a href="{{tracks.3.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">#5 {{tracks.4.title}}</div>
{% if tracks.4.album.cover_image %}
<a href="{{tracks.4.get_absolute_url}}"><img src="{{tracks.4.album.cover_image.url}}" width="150px"></a>
{% else %}
<a href="{{tracks.4.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="150px"></a>
{% endif %}
</div>
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#6 {{tracks.5.title}}</div>
{% if tracks.5.album.cover_image %}
<a href="{{tracks.5.get_absolute_url}}"><img src="{{tracks.5.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.5.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#7 {{tracks.6.title}}</div>
{% if tracks.6.album.cover_image %}
<a href="{{tracks.6.get_absolute_url}}"><img src="{{tracks.6.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.6.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#8 {{tracks.7.title}}</div>
{% if tracks.7.album.cover_image %}
<a href="{{tracks.7.get_absolute_url}}"><img src="{{tracks.7.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.7.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#9 {{tracks.8.title}}</div>
{% if tracks.8.album.cover_image %}
<a href="{{tracks.8.get_absolute_url}}"><img src="{{tracks.8.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.8.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#10 {{tracks.9.title}}</div>
{% if tracks.9.album.cover_image %}
<a href="{{tracks.9.get_absolute_url}}"><img src="{{tracks.9.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.9.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#11 {{tracks.10.title}}</div>
{% if tracks.10.album.cover_image %}
<a href="{{tracks.10.get_absolute_url}}"><img src="{{tracks.10.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.10.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#12 {{tracks.11.title}}</div>
{% if tracks.11.album.cover_image %}
<a href="{{tracks.11.get_absolute_url}}"><img src="{{tracks.11.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.11.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#13 {{tracks.12.title}}</div>
{% if tracks.12.album.cover_image %}
<a href="{{tracks.12.get_absolute_url}}"><img src="{{tracks.12.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.12.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#14 {{tracks.13.title}}</div>
{% if tracks.13.album.cover_image %}
<a href="{{tracks.13.get_absolute_url}}"><img src="{{tracks.13.album.cover_image.url}}" width="100px"></a>
{% else %}
<a href="{{tracks.13.get_absolute_url}}"><img src="{% static 'images/track-placeholder.jpg' %}" width="100px"></a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row">
<h2>Last Scrobbles</h2>
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> |
This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
</div>
<div class="row">
<ul class="nav nav-tabs" id="myTab" role="tablist">
@ -68,7 +402,6 @@
<div class="tab-content" id="myTabContent2">
<div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
aria-labelledby="latest-listened-tab">
<h2>Latest listened</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
@ -183,7 +516,7 @@
<div class="modal-header">
<h5 class="modal-title" id="importModalLabel">Import scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
<div aria-hidden="true">&times;</div>
</button>
</div>
<form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
@ -221,7 +554,7 @@
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
<div aria-hidden="true">&times;</div>
</button>
</div>
<form action="{% url 'scrobbles:export' %}" method="get">
@ -246,4 +579,53 @@
$('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') });
$('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') });
</script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script><!-- comment ------------------------------------------------->
/* globals Chart:false, feather:false */
(function () {
'use strict'
feather.replace({ 'aria-hidden': 'true' })
// Graphs
var ctx = document.getElementById('myChart')
// eslint-disable-next-line no-unused-vars
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: [
{% for day in weekly_data.keys %}
"{{day}}"{% if not forloop.last %},{% endif %}
{% endfor %}
],
datasets: [{
data: [
{% for count in weekly_data.values %}
{{count}}{% if not forloop.last %},{% endif %}
{% endfor %}
],
lineTension: 0,
backgroundColor: 'transparent',
borderColor: '#007bf0',
borderWidth: 4,
pointBackgroundColor: '#007bff'
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: false
}
}]
},
legend: {
display: false
}
}
})
})()
</script>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "base_detail.html" %}
{% block title %}{{object.title}} - {{object.round.season.league}}{% endblock %}
{% block details %}
<div class="row">
<h2>{{object.tv_series}}</h2>
<div class="col-md">
<h3>Last scrobbles</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Season</th>
<th scope="col">League</th>
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
<tr>
<td>{{scrobble.timestamp}}</td>
<td>{{scrobble.media_obj.round.season.name}}</td>
<td>{{scrobble.media_obj.round.season.league}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "base_list.html" %}
{% block title %}Sport events{% endblock %}
{% block lists %}
<div class="row">
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Date</th>
<th scope="col">Round</th>
<th scope="col">League</th>
<th scope="col">Scrobbles</th>
<th scope="col">All time</th>
</tr>
</thead>
<tbody>
{% for obj in object_list %}
<tr>
<td><a href="{{obj.get_absolute_url}}">{{obj.title}}</a></td>
<td>{{obj.start}}</td>
<td>{{obj.round.name}}</td>
<td>{{obj.round.league}}</td>
<td>{{obj.scrobble_set.count}}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,11 +1,9 @@
{% extends "base_detail.html" %}
{% block title %}{{object.name}}{% endblock %}
{% block title %}{{object.title}}{% if object.tv_series %} - {{object.tv_series}}{% endif %}{% endblock %}
{% block details %}
<div class="row">
<h2>{{object.tv_series}}</h2>
<div class="col-md">
<h3>Last scrobbles</h3>
<div class="table-responsive">
@ -14,9 +12,11 @@
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
{% if object.tv_series %}
<th scope="col">Series</th>
<th scope="col">Season</th>
<th scope="col">Episode</th>
{% endif %}
</tr>
</thead>
<tbody>
@ -24,9 +24,11 @@
<tr>
<td>{{scrobble.timestamp}}</td>
<td>{{scrobble.video.title}}</td>
{% if object.tv_series %}
<td>{{scrobble.video.tv_series}}</td>
<td>{{scrobble.video.season_number}}</td>
<td>{{scrobble.video.episode_number}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
@ -34,4 +36,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -1,12 +1,11 @@
import scrobbles.views as scrobbles_views
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from rest_framework import routers
import vrobbler.apps.scrobbles.views as scrobbles_views
from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
from vrobbler.apps.music import urls as music_urls
from vrobbler.apps.sports import urls as sports_urls
from vrobbler.apps.music.api.views import (
AlbumViewSet,
ArtistViewSet,
@ -23,7 +22,6 @@ from vrobbler.apps.scrobbles.api.views import (
from vrobbler.apps.sports.api.views import (
LeagueViewSet,
PlayerViewSet,
RoundViewSet,
SeasonViewSet,
SportEventViewSet,
SportViewSet,
@ -60,16 +58,9 @@ urlpatterns = [
path("accounts/", include("allauth.urls")),
path("", include(music_urls, namespace="music")),
path("", include(video_urls, namespace="videos")),
path("", include(sports_urls, namespace="sports")),
path("", include(scrobble_urls, namespace="scrobbles")),
path(
"", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"
),
]
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
)
urlpatterns += static(
settings.STATIC_URL, document_root=settings.STATIC_ROOT
)