Compare commits

...

12 Commits

Author SHA1 Message Date
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
22 changed files with 736 additions and 126 deletions

View File

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

View File

@ -129,7 +129,9 @@ class Album(TimeStampedModel):
return self.artists.first() return self.artists.first()
def scrape_theaudiodb(self) -> None: def scrape_theaudiodb(self) -> None:
artist = self.primary_artist.name artist = "Various Artists"
if self.primary_artist:
artist = self.primary_artist.name
album_data = lookup_album_from_tadb(self.name, artist) album_data = lookup_album_from_tadb(self.name, artist)
if not album_data.get('theaudiodb_id'): if not album_data.get('theaudiodb_id'):
logger.info(f"No data for {self} found in TheAudioDB") logger.info(f"No data for {self} found in TheAudioDB")

View File

@ -1,5 +1,4 @@
from datetime import timedelta from django.db.models import Count
from django.utils import timezone
from django.views import generic from django.views import generic
from music.models import Album, Artist, Track from music.models import Album, Artist, Track
from scrobbles.models import ChartRecord from scrobbles.models import ChartRecord
@ -22,7 +21,6 @@ class TrackDetailView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs) context_data = super().get_context_data(**kwargs)
context_data['charts'] = ChartRecord.objects.filter( context_data['charts'] = ChartRecord.objects.filter(
track=self.object, rank__in=[1, 2, 3] track=self.object, rank__in=[1, 2, 3]
) )
@ -34,7 +32,12 @@ class ArtistListView(generic.ListView):
paginate_by = 100 paginate_by = 100
def get_queryset(self): 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): def get_context_data(self, *, object_list=None, **kwargs):
context_data = super().get_context_data( context_data = super().get_context_data(
@ -50,6 +53,17 @@ class ArtistDetailView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context_data = super().get_context_data(**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( context_data['charts'] = ChartRecord.objects.filter(
artist=self.object, rank__in=[1, 2, 3] artist=self.object, rank__in=[1, 2, 3]
) )
@ -58,17 +72,14 @@ class ArtistDetailView(generic.DetailView):
class AlbumListView(generic.ListView): class AlbumListView(generic.ListView):
model = Album model = Album
paginate_by = 50
def get_queryset(self): def get_queryset(self):
return super().get_queryset().order_by("name") return (
super()
def get_context_data(self, *, object_list=None, **kwargs): .get_queryset()
context_data = super().get_context_data( .annotate(scrobble_count=Count('track__scrobble'))
object_list=object_list, **kwargs .order_by("-scrobble_count")
) )
context_data['view'] = self.request.GET.get('view')
return context_data
class AlbumDetailView(generic.DetailView): class AlbumDetailView(generic.DetailView):

View File

@ -1,23 +1,18 @@
import datetime
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.utils import timezone 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 # 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): def to_user_timezone(date, profile):
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
return date.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)).astimezone( return date.astimezone(pytz.timezone(timezone))
pytz.timezone(timezone)
)
def to_system_timezone(date, profile): def to_system_timezone(date):
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE return date.astimezone(pytz.timezone(settings.TIME_ZONE))
return date.replace(tzinfo=pytz.timezone(timezone)).astimezone(
pytz.timezone(settings.TIME_ZONE)
)
def now_user_timezone(profile): def now_user_timezone(profile):
@ -25,9 +20,39 @@ def now_user_timezone(profile):
return timezone.localtime(timezone.now()) return timezone.localtime(timezone.now())
def now_system_timezone(): def start_of_day(dt, profile) -> datetime:
return ( """Get the start of the day in the profile's timezone"""
datetime.datetime.now() timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)) tzinfo = pytz.timezone(timezone)
.astimezone(pytz.timezone(settings.TIME_ZONE)) 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 calendar
import datetime
import logging import logging
from uuid import uuid4 from uuid import uuid4
@ -15,6 +16,14 @@ from scrobbles.utils import check_scrobble_for_finish
from sports.models import SportEvent from sports.models import SportEvent
from videos.models import Series, Video 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 from vrobbler.apps.scrobbles.stats import build_charts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -62,6 +71,7 @@ class BaseFileImportMixin(TimeStampedModel):
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
if not self.process_log: if not self.process_log:
logger.warning("No lines in process log found to undo") logger.warning("No lines in process log found to undo")
return return
@ -256,6 +266,26 @@ class ChartRecord(TimeStampedModel):
series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL) series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
artist = models.ForeignKey(Artist, 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) 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 @property
def media_obj(self): def media_obj(self):

View File

@ -6,8 +6,11 @@ from typing import Optional
import pytz import pytz
from django.apps import apps from django.apps import apps
from django.conf import settings 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__) logger = logging.getLogger(__name__)
@ -102,11 +105,11 @@ def get_scrobble_count_qs(
def build_charts( def build_charts(
user: "User",
year: Optional[int] = None, year: Optional[int] = None,
month: Optional[int] = None, month: Optional[int] = None,
week: Optional[int] = None, week: Optional[int] = None,
day: Optional[int] = None, day: Optional[int] = None,
user=None,
model_str="Track", model_str="Track",
): ):
ChartRecord = apps.get_model( ChartRecord = apps.get_model(
@ -140,3 +143,94 @@ def build_charts(
ChartRecord.objects.bulk_create( ChartRecord.objects.bulk_create(
chart_records, ignore_conflicts=True, batch_size=500 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 import logging
from celery import shared_task
from celery import shared_task
from django.contrib.auth import get_user_model
from scrobbles.models import ( from scrobbles.models import (
AudioScrobblerTSVImport, AudioScrobblerTSVImport,
KoReaderImport, KoReaderImport,
LastFmImport, LastFmImport,
) )
from vrobbler.apps.scrobbles.stats import build_yesterdays_charts_for_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model()
@shared_task @shared_task
@ -35,3 +39,9 @@ def process_koreader_import(import_id):
logger.warn(f"KOReaderImport not found with id {import_id}") logger.warn(f"KOReaderImport not found with id {import_id}")
koreader_import.process() koreader_import.process()
@shared_task
def create_yesterdays_charts():
for user in User.objects.all():
build_yesterdays_charts_for_user(user)

View File

@ -58,13 +58,16 @@ def lookup_album_from_tadb(name: str, artist: str) -> dict:
album_info['theaudiodb_mood'] = album.get('strMood') album_info['theaudiodb_mood'] = album.get('strMood')
album_info['theaudiodb_speed'] = album.get('strSpeed') album_info['theaudiodb_speed'] = album.get('strSpeed')
album_info['theaudiodb_theme'] = album.get('strTheme') album_info['theaudiodb_theme'] = album.get('strTheme')
album_info['theaudiodb_year_released'] = album.get('intYearReleased')
album_info['allmusic_id'] = album.get('strAllMusicID') album_info['allmusic_id'] = album.get('strAllMusicID')
album_info['wikipedia_slug'] = album.get('strWikipediaID') album_info['wikipedia_slug'] = album.get('strWikipediaID')
album_info['discogs_id'] = album.get('strDiscogsID') album_info['discogs_id'] = album.get('strDiscogsID')
album_info['wikidata_id'] = album.get('strWikidataID') album_info['wikidata_id'] = album.get('strWikidataID')
album_info['rateyourmusic_id'] = album.get('strRateYourMusicID') album_info['rateyourmusic_id'] = album.get('strRateYourMusicID')
if album.get('intYearReleased'):
album_info['theaudiodb_year_released'] = float(
album.get('intYearReleased')
)
if album.get('intScore'): if album.get('intScore'):
album_info['theaudiodb_score'] = float(album.get('intScore')) album_info['theaudiodb_score'] = float(album.get('intScore'))
if album.get('intScoreVotes'): if album.get('intScoreVotes'):

View File

@ -67,31 +67,64 @@ class RecentScrobbleList(ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
view = self.request.GET.get('view', 'vrobbler')
user = self.request.user user = self.request.user
if user.is_authenticated: if user.is_authenticated:
if view == 'vrobbler':
completed_for_user = Scrobble.objects.filter(
played_to_completion=True, user=user
)
data['video_scrobble_list'] = completed_for_user.filter(
video__isnull=False
).order_by('-timestamp')[:15]
completed_for_user = Scrobble.objects.filter( data['podcast_scrobble_list'] = completed_for_user.filter(
played_to_completion=True, user=user podcast_episode__isnull=False
) ).order_by('-timestamp')[:15]
data['video_scrobble_list'] = completed_for_user.filter(
video__isnull=False
).order_by('-timestamp')[:15]
data['podcast_scrobble_list'] = completed_for_user.filter( data['sport_scrobble_list'] = completed_for_user.filter(
podcast_episode__isnull=False sport_event__isnull=False
).order_by('-timestamp')[:15] ).order_by('-timestamp')[:15]
data['sport_scrobble_list'] = completed_for_user.filter( data[
sport_event__isnull=False 'active_imports'
).order_by('-timestamp')[:15] ] = AudioScrobblerTSVImport.objects.filter(
data['active_imports'] = AudioScrobblerTSVImport.objects.filter( processing_started__isnull=False,
processing_started__isnull=False, processed_finished__isnull=True,
processed_finished__isnull=True, user=self.request.user,
user=self.request.user, )
)
if view == "maloja":
artist_params = {'user': user, 'media_type': 'Artist'}
data['current_artist_charts'] = {
"today": live_charts(
**artist_params, chart_period="today", limit=14
),
"week": live_charts(
**artist_params, chart_period="week", limit=14
),
"month": live_charts(
**artist_params, chart_period="month", limit=14
),
"all": live_charts(**artist_params, limit=14),
}
track_params = {'user': user, 'media_type': 'Track'}
data['current_track_charts'] = {
"today": live_charts(
**track_params, chart_period="today", limit=14
),
"week": live_charts(
**track_params, chart_period="week", limit=14
),
"month": live_charts(
**track_params, chart_period="month", limit=14
),
"all": live_charts(**track_params, limit=14),
}
data["view"] = view
data["weekly_data"] = week_of_scrobbles(user=user) data["weekly_data"] = week_of_scrobbles(user=user)
data['counts'] = scrobble_counts(user) data['counts'] = scrobble_counts(user)
data['imdb_form'] = ScrobbleForm data['imdb_form'] = ScrobbleForm
data['export_form'] = ExportScrobbleForm data['export_form'] = ExportScrobbleForm

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

@ -302,54 +302,6 @@
</div> </div>
</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 %} {% block extra_js %}
{% endblock %} {% endblock %}
</body> </body>

View File

@ -46,17 +46,17 @@
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Scrobbles</th>
<th scope="col">Album</th> <th scope="col">Album</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
<th scope="col">Scrobbles</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for album in object_list %} {% for album in object_list %}
<tr> <tr>
<td><a href="{{album.get_absolute_url}}">{{album}}</a></td>
<td><a href="{{album.artist.get_absolute_url}}">{{album.artist}}</a></td>
<td>{{album.scrobbles.count}}</td> <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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -44,15 +44,15 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for track in object.tracks %} {% for track in tracks_ranked %}
<tr> <tr>
<td>{{rank}}#1</td> <td>#{{track.0}}</td>
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td> <td><a href="{{track.1.get_absolute_url}}">{{track.1.title}}</a></td>
<td><a href="{{track.album.get_absolute_url}}">{{track.album}}</a></td> <td><a href="{{track.1.album.get_absolute_url}}">{{track.1.album}}</a></td>
<td>{{track.scrobble_count}}</td> <td>{{track.1.scrobble_count}}</td>
<td> <td>
<div class="progress-bar" style="margin-right:5px;"> <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> </div>
</td> </td>
</tr> </tr>

View File

@ -45,17 +45,15 @@
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Artist</th>
<th scope="col">Scrobbles</th> <th scope="col">Scrobbles</th>
<th scope="col">All time</th> <th scope="col">Artist</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for artist in object_list %} {% for artist in object_list %}
<tr> <tr>
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
<td>{{artist.scrobbles.count}}</td> <td>{{artist.scrobbles.count}}</td>
<td></td> <td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -24,17 +24,17 @@
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Scrobbles</th>
<th scope="col">Track</th> <th scope="col">Track</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
<th scope="col">Scrobbles</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for track in object_list %} {% for track in object_list %}
<tr> <tr>
<td>{{track.scrobble_set.count}}</td>
<td><a href="{{track.get_absolute_url}}">{{track}}</a></td> <td><a href="{{track.get_absolute_url}}">{{track}}</a></td>
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td> <td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
<td>{{track.scrobble_set.count}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -1,12 +1,51 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load humanize %} {% load humanize %}
{% block head_extra %}
<style>
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%;
background:rgba(0,0,0,0.3);
}
.caption-medium {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 75%;
background:rgba(0,0,0,0.3);
}
.caption-small {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 60%;
background:rgba(0,0,0,0.3);
}
.maloja-chart img { border: 1px solid #ccc; }
</style>
{% endblock %}
{% block content %} {% block content %}
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4"> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1> <h1 class="h2">Dashboard</h1><span><a href="/?view=maloja">like maloja</a></span>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="btn-group me-2"> <div class="btn-group me-2">
@ -25,7 +64,7 @@
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton" <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span data-feather="calendar"></span> <div data-feather="calendar"></div>
This week This week
</button> </button>
<div class="dropdown-menu" data-bs-toggle="#graphDataChange" aria-labelledby="graphDateButton"> <div class="dropdown-menu" data-bs-toggle="#graphDataChange" aria-labelledby="graphDateButton">
@ -36,14 +75,253 @@
</div> </div>
</div> </div>
{% if view == "vrobbler" %}
<canvas class="my-4 w-100" id="myChart" width="900" height="220"></canvas> <canvas class="my-4 w-100" id="myChart" width="900" height="220"></canvas>
{% endif %}
<div class="container"> <div class="container">
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if view == "maloja" %}
<div class="row"> <div class="row">
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> | <h2>Top Artist</h2>
This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p> <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.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}}
</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.first %}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">{{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>
{% 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">{{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>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.2.name}}</div>
{% if artists.2.thumbnail %}
<a href="{{artists.2.get_absolute_url}}"><img lt="{{artists.2.name}}" src="{{artists.2.thumbnail.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.3.name}}</div>
{% if artists.3.thumbnail %}
<a href="{{artists.3.get_absolute_url}}"><img lt="{{artists.3.name}}" src="{{artists.3.thumbnail.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.4.name}}</div>
{% if artists.4.thumbnail %}
<a href="{{artists.4.get_absolute_url}}"><img lt="{{artists.4.name}}" src="{{artists.4.thumbnail.url}}" 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">{{artists.5.name}}</div>
{% if artists.5.thumbnail %}
<a href="{{artists.5.get_absolute_url}}"><img lt="{{artists.5.name}}" src="{{artists.5.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.6.name}}</div>
{% if artists.6.thumbnail %}
<a href="{{artists.6.get_absolute_url}}"><img lt="{{artists.6.name}}" src="{{artists.6.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.7.name}}</div>
{% if artists.7.thumbnail %}
<a href="{{artists.7.get_absolute_url}}"><img lt="{{artists.7.name}}" src="{{artists.7.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.8.name}}</div>
{% if artists.8.thumbnail %}
<a href="{{artists.8.get_absolute_url}}"><img lt="{{artists.8.name}}" src="{{artists.8.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.9.name}}</div>
{% if artists.9.thumbnail %}
<a href="{{artists.9.get_absolute_url}}"><img lt="{{artists.9.name}}" src="{{artists.9.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.10.name}}</div>
{% if artists.10.thumbnail %}
<a href="{{artists.10.get_absolute_url}}"><img lt="{{artists.10.name}}" src="{{artists.10.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.11.name}}</div>
{% if artists.11.thumbnail %}
<a href="{{artists.11.get_absolute_url}}"><img lt="{{artists.11.name}}" src="{{artists.11.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.12.name}}</div>
{% if artists.12thumbnail %}
<a href="{{artists.12.get_absolute_url}}"><img lt="{{artists.13.name}}" src="{{artists.12.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.13.name}}</div>
{% if artists.13.thumbnail %}
<a href="{{artists.13.get_absolute_url}}"><img lt="{{artists.13.name}}" src="{{artists.13.thumbnail.url}}" 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.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}}
</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.first %}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">{{tracks.0.title}}</div>
{% if tracks.0.album.cover_image %}
<a href="{{tracks.0.get_absolute_url}}"><img lt="{{tracks.0.title}}" src="{{tracks.0.album.cover_image.url}}" 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">{{tracks.1.title}}</div>
{% if tracks.1.album.cover_image %}
<a href="{{tracks.1.get_absolute_url}}"><img lt="{{tracks.1.title}}" src="{{tracks.1.album.cover_image.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.2.title}}</div>
{% if tracks.2.album.cover_image %}
<a href="{{tracks.2.get_absolute_url}}"><img lt="{{tracks.2.title}}" src="{{tracks.2.album.cover_image.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.3.title}}</div>
{% if tracks.3.album.cover_image %}
<a href="{{tracks.3.get_absolute_url}}"><img lt="{{tracks.3.title}}" src="{{tracks.3.album.cover_image.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.4.title}}</div>
{% if tracks.4.album.cover_image %}
<a href="{{tracks.4.get_absolute_url}}"><img lt="{{tracks.4.title}}" src="{{tracks.4.album.cover_image.urL}}" 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">{{tracks.5.title}}</div>
{% if tracks.5.album.cover_image %}
<a href="{{tracks.5.get_absolute_url}}"><img lt="{{tracks.5.title}}" src="{{tracks.5.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.6.title}}</div>
{% if tracks.6.album.cover_image %}
<a href="{{tracks.6.get_absolute_url}}"><img lt="{{tracks.6.title}}" src="{{tracks.6.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.7.title}}</div>
{% if tracks.7.album.cover_image %}
<a href="{{track.7.get_absolute_url}}"><img lt="{{tracks.7.title}}" src="{{tracks.7.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.8.title}}</div>
{% if tracks.8.album.cover_image %}
<a href="{{track.8.get_absolute_url}}"><img lt="{{tracks.8.title}}" src="{{tracks.8.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.9.title}}</div>
{% if tracks.9.album.cover_image %}
<a href="{{track.9.get_absolute_url}}"><img lt="{{tracks.9.title}}" src="{{tracks.9.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.10.title}}</div>
{% if tracks.10.album.cover_image %}
<a href="{{tracks.10.get_absolute_url}}"><img lt="{{tracks.10.title}}" src="{{tracks.10.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.11.title}}</div>
{% if tracks.11.album.cover_image %}
<a href="{{track.11.get_absolute_url}}"><img lt="{{tracks.11.title}}" src="{{tracks.11.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.12.title}}</div>
{% if tracks.12.album.cover_image %}
<a href="{{track.12.get_absolute_url}}"><img lt="{{tracks.12.title}}" src="{{tracks.12.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.13.title}}</div>
{% if tracks.13.album.cover_image %}
<a href="{{track.13.get_absolute_url}}"><img lt="{{tracks.13.title}}" src="{{tracks.13.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<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>
<div class="row"> <div class="row">
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
@ -68,7 +346,6 @@
<div class="tab-content" id="myTabContent2"> <div class="tab-content" id="myTabContent2">
<div class="tab-pane fade show active" id="latest-listened" role="tabpanel" <div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
aria-labelledby="latest-listened-tab"> aria-labelledby="latest-listened-tab">
<h2>Latest listened</h2>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
@ -183,7 +460,7 @@
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="importModalLabel">Import scrobbles</h5> <h5 class="modal-title" id="importModalLabel">Import scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close"> <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> </button>
</div> </div>
<form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data"> <form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
@ -221,7 +498,7 @@
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5> <h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close"> <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> </button>
</div> </div>
<form action="{% url 'scrobbles:export' %}" method="get"> <form action="{% url 'scrobbles:export' %}" method="get">
@ -246,4 +523,53 @@
$('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') }); $('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') });
$('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') }); $('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') });
</script> </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 %} {% 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" %} {% extends "base_detail.html" %}
{% block title %}{{object.name}}{% endblock %} {% block title %}{{object.title}}{% if object.tv_series %} - {{object.tv_series}}{% endif %}{% endblock %}
{% block details %} {% block details %}
<div class="row"> <div class="row">
<h2>{{object.tv_series}}</h2>
<div class="col-md"> <div class="col-md">
<h3>Last scrobbles</h3> <h3>Last scrobbles</h3>
<div class="table-responsive"> <div class="table-responsive">
@ -14,9 +12,11 @@
<tr> <tr>
<th scope="col">Date</th> <th scope="col">Date</th>
<th scope="col">Title</th> <th scope="col">Title</th>
{% if object.tv_series %}
<th scope="col">Series</th> <th scope="col">Series</th>
<th scope="col">Season</th> <th scope="col">Season</th>
<th scope="col">Episode</th> <th scope="col">Episode</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -24,9 +24,11 @@
<tr> <tr>
<td>{{scrobble.timestamp}}</td> <td>{{scrobble.timestamp}}</td>
<td>{{scrobble.video.title}}</td> <td>{{scrobble.video.title}}</td>
{% if object.tv_series %}
<td>{{scrobble.video.tv_series}}</td> <td>{{scrobble.video.tv_series}}</td>
<td>{{scrobble.video.season_number}}</td> <td>{{scrobble.video.season_number}}</td>
<td>{{scrobble.video.episode_number}}</td> <td>{{scrobble.video.episode_number}}</td>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -34,4 +36,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,6 +5,7 @@ from rest_framework import routers
import vrobbler.apps.scrobbles.views as scrobbles_views import vrobbler.apps.scrobbles.views as scrobbles_views
from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
from vrobbler.apps.music import urls as music_urls 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 ( from vrobbler.apps.music.api.views import (
AlbumViewSet, AlbumViewSet,
ArtistViewSet, ArtistViewSet,
@ -57,6 +58,7 @@ urlpatterns = [
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("", include(music_urls, namespace="music")), path("", include(music_urls, namespace="music")),
path("", include(video_urls, namespace="videos")), path("", include(video_urls, namespace="videos")),
path("", include(sports_urls, namespace="sports")),
path("", include(scrobble_urls, namespace="scrobbles")), path("", include(scrobble_urls, namespace="scrobbles")),
path( path(
"", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home" "", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"