Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34a2339b3b | |||
| 34abbe753b | |||
| 0fe00c3dd8 | |||
| 5a3eb7a8c8 | |||
| e63ca13d57 | |||
| b3d3098fe0 | |||
| 8f5a200526 | |||
| 411d2b42b0 | |||
| bce1322289 | |||
| 908819d24e | |||
| 6d21bb2e85 | |||
| 7df3fedc64 | |||
| b4e83b184e | |||
| 6e885df1dd | |||
| f153f831b3 | |||
| 66a90c87f1 | |||
| 6e17e4ce0d | |||
| 3c3e567573 | |||
| 2775851474 | |||
| 654a64e82d | |||
| 7dd7f369d8 | |||
| fb6110c71d | |||
| 93299a1abd | |||
| a58ddebd23 | |||
| 41cdb96e94 | |||
| 5a8e828b81 | |||
| c84a3072be | |||
| 0bd7ed4463 | |||
| ee232aa103 | |||
| 7151646600 | |||
| 1d7cf965ef | |||
| 0a9279dbd4 | |||
| bf3479dbc7 | |||
| a99dca246b | |||
| f76aaf6a9c | |||
| ce1541bb2d | |||
| d34e56aa89 | |||
| 6316d4bead | |||
| 56e5728245 | |||
| 6ff170e169 | |||
| 86d1cf0d65 | |||
| a0101bf1ae | |||
| 457afdc9ef |
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "0.11.0"
|
||||
version = "0.11.12"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
|
||||
@ -5,12 +5,7 @@ import time_machine
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from music.aggregators import (
|
||||
scrobble_counts,
|
||||
top_artists,
|
||||
top_tracks,
|
||||
week_of_scrobbles,
|
||||
)
|
||||
from music.aggregators import live_charts, scrobble_counts, week_of_scrobbles
|
||||
from profiles.models import UserProfile
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
@ -55,7 +50,7 @@ def test_week_of_scrobbles_data(client, mopidy_track_request_data):
|
||||
def test_top_tracks_by_day(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user)
|
||||
tops = live_charts(user)
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@ -63,7 +58,7 @@ def test_top_tracks_by_day(client, mopidy_track_request_data):
|
||||
def test_top_tracks_by_week(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user, filter='week')
|
||||
tops = live_charts(user, chart_period='week')
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@ -71,7 +66,7 @@ def test_top_tracks_by_week(client, mopidy_track_request_data):
|
||||
def test_top_tracks_by_month(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user, filter='month')
|
||||
tops = live_charts(user, chart_period='month')
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@ -79,7 +74,7 @@ def test_top_tracks_by_month(client, mopidy_track_request_data):
|
||||
def test_top_tracks_by_year(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user, filter='year')
|
||||
tops = live_charts(user, chart_period='year')
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@ -87,7 +82,7 @@ def test_top_tracks_by_year(client, mopidy_track_request_data):
|
||||
def test_top__artists_by_week(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_artists(user, filter='week')
|
||||
tops = live_charts(user, chart_period='week', media_type="Artist")
|
||||
assert tops[0].name == "Sublime"
|
||||
|
||||
|
||||
@ -95,7 +90,7 @@ def test_top__artists_by_week(client, mopidy_track_request_data):
|
||||
def test_top__artists_by_month(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_artists(user, filter='month')
|
||||
tops = live_charts(user, chart_period='month', media_type="Artist")
|
||||
assert tops[0].name == "Sublime"
|
||||
|
||||
|
||||
@ -103,5 +98,5 @@ def test_top__artists_by_month(client, mopidy_track_request_data):
|
||||
def test_top__artists_by_year(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_artists(user, filter='year')
|
||||
tops = live_charts(user, chart_period='year', media_type="Artist")
|
||||
assert tops[0].name == "Sublime"
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import pytest
|
||||
import imdb
|
||||
from mock import patch
|
||||
|
||||
from vrobbler.apps.scrobbles.imdb import lookup_video_from_imdb
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Need to sort out third party API testing")
|
||||
def test_lookup_imdb_bad_id(caplog):
|
||||
data = lookup_video_from_imdb('3409324')
|
||||
assert data is None
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,22 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import pytz
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.apps import apps
|
||||
from django.db.models import Count, Q, QuerySet
|
||||
from django.utils import timezone
|
||||
from music.models import Artist, Track
|
||||
from scrobbles.models import Scrobble
|
||||
from videos.models import Video
|
||||
from vrobbler.apps.profiles.utils import now_user_timezone
|
||||
|
||||
NOW = timezone.now()
|
||||
START_OF_TODAY = datetime.combine(NOW.date(), datetime.min.time(), NOW.tzinfo)
|
||||
STARTING_DAY_OF_CURRENT_WEEK = NOW.date() - timedelta(
|
||||
days=NOW.today().isoweekday() % 7
|
||||
)
|
||||
STARTING_DAY_OF_CURRENT_MONTH = NOW.date().replace(day=1)
|
||||
STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
|
||||
|
||||
|
||||
def scrobble_counts(user=None):
|
||||
|
||||
@ -78,13 +70,13 @@ def week_of_scrobbles(
|
||||
media_filter = Q(video__video_type=Video.VideoType.TV_EPISODE)
|
||||
|
||||
for day in range(6, -1, -1):
|
||||
start = start - timedelta(days=day)
|
||||
end = datetime.combine(start, datetime.max.time(), now.tzinfo)
|
||||
day_of_week = start.strftime('%A')
|
||||
start_day = start - timedelta(days=day)
|
||||
end = datetime.combine(start_day, datetime.max.time(), now.tzinfo)
|
||||
day_of_week = start_day.strftime('%A')
|
||||
|
||||
scrobble_day_dict[day_of_week] = base_qs.filter(
|
||||
media_filter,
|
||||
timestamp__gte=start,
|
||||
timestamp__gte=start_day,
|
||||
timestamp__lte=end,
|
||||
played_to_completion=True,
|
||||
).count()
|
||||
@ -92,58 +84,64 @@ def week_of_scrobbles(
|
||||
return scrobble_day_dict
|
||||
|
||||
|
||||
def top_tracks(
|
||||
user: "User", filter: str = "today", limit: int = 15
|
||||
) -> List["Track"]:
|
||||
|
||||
def live_charts(
|
||||
user: "User",
|
||||
media_type: str = "Track",
|
||||
chart_period: str = "all",
|
||||
limit: int = 15,
|
||||
) -> QuerySet:
|
||||
now = timezone.now()
|
||||
tzinfo = now.tzinfo
|
||||
now = now.date()
|
||||
if user.is_authenticated:
|
||||
now = now_user_timezone(user.profile)
|
||||
tzinfo = now.tzinfo
|
||||
|
||||
start_of_today = datetime.combine(
|
||||
now.date(), datetime.min.time(), now.tzinfo
|
||||
)
|
||||
starting_day_of_current_week = now.date() - timedelta(
|
||||
days=now.today().isoweekday() % 7
|
||||
)
|
||||
starting_day_of_current_month = now.date().replace(day=1)
|
||||
starting_day_of_current_year = now.date().replace(month=1, day=1)
|
||||
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
|
||||
start_day_of_week = now - timedelta(days=now.today().isoweekday() % 7)
|
||||
start_day_of_month = now.replace(day=1)
|
||||
start_day_of_year = now.replace(month=1, day=1)
|
||||
|
||||
time_filter = Q(scrobble__timestamp__gte=start_of_today)
|
||||
if filter == "week":
|
||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_week)
|
||||
if filter == "month":
|
||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_month)
|
||||
if filter == "year":
|
||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_year)
|
||||
media_model = apps.get_model(app_label='music', model_name=media_type)
|
||||
|
||||
period_queries = {
|
||||
'today': {'scrobble__timestamp__gte': start_of_today},
|
||||
'week': {'scrobble__timestamp__gte': start_day_of_week},
|
||||
'month': {'scrobble__timestamp__gte': start_day_of_month},
|
||||
'year': {'scrobble__timestamp__gte': start_day_of_year},
|
||||
'all': {},
|
||||
}
|
||||
|
||||
time_filter = Q()
|
||||
completion_filter = Q(
|
||||
scrobble__user=user, scrobble__played_to_completion=True
|
||||
)
|
||||
user_filter = Q(scrobble__user=user)
|
||||
count_field = "scrobble"
|
||||
|
||||
if media_type == "Artist":
|
||||
for period, query_dict in period_queries.items():
|
||||
period_queries[period] = {
|
||||
"track__" + k: v for k, v in query_dict.items()
|
||||
}
|
||||
completion_filter = Q(
|
||||
track__scrobble__user=user,
|
||||
track__scrobble__played_to_completion=True,
|
||||
)
|
||||
count_field = "track__scrobble"
|
||||
user_filter = Q(track__scrobble__user=user)
|
||||
|
||||
time_filter = Q(**period_queries[chart_period])
|
||||
|
||||
return (
|
||||
Track.objects.filter(time_filter)
|
||||
.annotate(num_scrobbles=Count("scrobble", distinct=True))
|
||||
.order_by("-num_scrobbles")[:limit]
|
||||
)
|
||||
|
||||
|
||||
def top_artists(
|
||||
user: "User", filter: str = "today", limit: int = 15
|
||||
) -> List["Artist"]:
|
||||
time_filter = Q(track__scrobble__timestamp__gte=START_OF_TODAY)
|
||||
if filter == "week":
|
||||
time_filter = Q(
|
||||
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
|
||||
media_model.objects.filter(user_filter, time_filter)
|
||||
.annotate(
|
||||
num_scrobbles=Count(
|
||||
count_field,
|
||||
filter=completion_filter,
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
if filter == "month":
|
||||
time_filter = Q(
|
||||
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
|
||||
)
|
||||
if filter == "year":
|
||||
time_filter = Q(
|
||||
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
|
||||
)
|
||||
|
||||
return (
|
||||
Artist.objects.filter(time_filter)
|
||||
.annotate(num_scrobbles=Count("track__scrobble", distinct=True))
|
||||
.order_by("-num_scrobbles")[:limit]
|
||||
)
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-27 03:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0009_alter_track_musicbrainz_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='artist',
|
||||
name='biography',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artist',
|
||||
name='theaudiodb_genre',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artist',
|
||||
name='theaudiodb_mood',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
20
vrobbler/apps/music/migrations/0011_artist_thumbnail.py
Normal file
20
vrobbler/apps/music/migrations/0011_artist_thumbnail.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-27 04:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0010_artist_biography_artist_theaudiodb_genre_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='artist',
|
||||
name='thumbnail',
|
||||
field=models.ImageField(
|
||||
blank=True, null=True, upload_to='artist/'
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -1,15 +1,19 @@
|
||||
import logging
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Dict, Optional
|
||||
from urllib.request import urlopen
|
||||
from uuid import uuid4
|
||||
|
||||
import musicbrainzngs
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.base import ContentFile, File
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
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}
|
||||
@ -18,7 +22,11 @@ BNULL = {"blank": True, "null": True}
|
||||
class Artist(TimeStampedModel):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
name = models.CharField(max_length=255)
|
||||
biography = models.TextField(**BNULL)
|
||||
theaudiodb_genre = models.CharField(max_length=255, **BNULL)
|
||||
theaudiodb_mood = models.CharField(max_length=255, **BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, **BNULL)
|
||||
thumbnail = models.ImageField(upload_to="artist/", **BNULL)
|
||||
|
||||
class Meta:
|
||||
unique_together = [['name', 'musicbrainz_id']]
|
||||
@ -53,6 +61,22 @@ class Artist(TimeStampedModel):
|
||||
|
||||
return ChartRecord.objects.filter(track__artist=self).order_by('-year')
|
||||
|
||||
def fix_metadata(self):
|
||||
tadb_info = lookup_artist_from_tadb(self.name)
|
||||
if not tadb_info:
|
||||
logger.warn(f"No response from TADB for artist {self.name}")
|
||||
return
|
||||
|
||||
self.biography = tadb_info['biography']
|
||||
self.theaudiodb_genre = tadb_info['genre']
|
||||
self.theaudiodb_mood = tadb_info['mood']
|
||||
|
||||
img_temp = NamedTemporaryFile(delete=True)
|
||||
img_temp.write(urlopen(tadb_info['thumb_url']).read())
|
||||
img_temp.flush()
|
||||
img_filename = f"{self.name}_{self.uuid}.jpg"
|
||||
self.thumbnail.save(img_filename, File(img_temp))
|
||||
|
||||
|
||||
class Album(TimeStampedModel):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
@ -63,14 +87,58 @@ 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
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("music:album_detail", kwargs={'slug': self.uuid})
|
||||
|
||||
def scrobbles(self):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
return Scrobble.objects.filter(
|
||||
track__in=self.track_set.all()
|
||||
).order_by('-timestamp')
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
return (
|
||||
self.track_set.all()
|
||||
.annotate(scrobble_count=models.Count('scrobble'))
|
||||
.order_by('-scrobble_count')
|
||||
)
|
||||
|
||||
@property
|
||||
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
|
||||
@ -118,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:
|
||||
@ -156,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)
|
||||
|
||||
@ -6,6 +6,11 @@ app_name = 'music'
|
||||
|
||||
urlpatterns = [
|
||||
path('albums/', views.AlbumListView.as_view(), name='albums_list'),
|
||||
path(
|
||||
'album/<slug:slug>/',
|
||||
views.AlbumDetailView.as_view(),
|
||||
name='album_detail',
|
||||
),
|
||||
path("tracks/", views.TrackListView.as_view(), name='tracks_list'),
|
||||
path(
|
||||
'tracks/<slug:slug>/',
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from musicbrainzngs.caa import musicbrainz
|
||||
|
||||
from scrobbles.musicbrainz import (
|
||||
lookup_album_dict_from_mb,
|
||||
lookup_artist_from_mb,
|
||||
@ -21,84 +23,68 @@ def get_or_create_artist(name: str, mbid: str = None) -> Artist:
|
||||
name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip()
|
||||
if 'featuring' in name.lower():
|
||||
name = re.split("featuring", name, flags=re.IGNORECASE)[0].strip()
|
||||
if '&' in name.lower():
|
||||
name = re.split("&", name, flags=re.IGNORECASE)[0].strip()
|
||||
|
||||
artist_dict = lookup_artist_from_mb(name)
|
||||
mbid = mbid or artist_dict['id']
|
||||
|
||||
logger.debug(f'Looking up artist {name} and mbid: {mbid}')
|
||||
artist, created = Artist.objects.get_or_create(
|
||||
name=name, musicbrainz_id=mbid
|
||||
)
|
||||
|
||||
logger.debug(f"Cleaning artist {name} with {artist_dict['name']}")
|
||||
# Clean up bad names in our DB with MB names
|
||||
# if artist.name != artist_dict["name"]:
|
||||
# artist.name = artist_dict["name"]
|
||||
# artist.save(update_fields=["name"])
|
||||
artist = Artist.objects.filter(musicbrainz_id=mbid).first()
|
||||
if not artist:
|
||||
artist = Artist.objects.create(name=name, musicbrainz_id=mbid)
|
||||
logger.debug(
|
||||
f"Created artist {artist.name} ({artist.musicbrainz_id}) "
|
||||
)
|
||||
artist.fix_metadata()
|
||||
|
||||
return 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
|
||||
|
||||
|
||||
def get_or_create_track(
|
||||
title: str,
|
||||
mbid: str,
|
||||
artist: Artist,
|
||||
album: Album,
|
||||
mbid: str = None,
|
||||
run_time=None,
|
||||
run_time_ticks=None,
|
||||
) -> Track:
|
||||
track = None
|
||||
if mbid:
|
||||
track = Track.objects.filter(
|
||||
musicbrainz_id=mbid,
|
||||
).first()
|
||||
if not track:
|
||||
track = Track.objects.filter(
|
||||
title=title, artist=artist, album=album
|
||||
).first()
|
||||
|
||||
if not mbid:
|
||||
mbid = lookup_track_from_mb(
|
||||
title, artist.musicbrainz_id, album.musicbrainz_id
|
||||
title,
|
||||
artist.musicbrainz_id,
|
||||
album.musicbrainz_id,
|
||||
)['id']
|
||||
|
||||
track = Track.objects.filter(musicbrainz_id=mbid).first()
|
||||
|
||||
if not track:
|
||||
track = Track.objects.create(
|
||||
title=title,
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from django.db.models import Count
|
||||
from django.views import generic
|
||||
from music.models import Track, Artist, Album
|
||||
from music.models import Album, Artist, Track
|
||||
from scrobbles.models import ChartRecord
|
||||
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(
|
||||
@ -17,18 +19,76 @@ class TrackDetailView(generic.DetailView):
|
||||
model = Track
|
||||
slug_field = 'uuid'
|
||||
|
||||
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]
|
||||
)
|
||||
return context_data
|
||||
|
||||
|
||||
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):
|
||||
model = Artist
|
||||
slug_field = 'uuid'
|
||||
|
||||
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]
|
||||
)
|
||||
return context_data
|
||||
|
||||
|
||||
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
|
||||
slug_field = 'uuid'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
# context_data['charts'] = ChartRecord.objects.filter(
|
||||
# track__album=self.object, rank__in=[1, 2, 3]
|
||||
# )
|
||||
return context_data
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -47,6 +47,7 @@ class ChartRecordAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"user",
|
||||
"rank",
|
||||
"count",
|
||||
"year",
|
||||
"week",
|
||||
"month",
|
||||
|
||||
17
vrobbler/apps/scrobbles/context_processors.py
Normal file
17
vrobbler/apps/scrobbles/context_processors.py
Normal file
@ -0,0 +1,17 @@
|
||||
import pytz
|
||||
from django.utils import timezone
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
def now_playing(request):
|
||||
user = request.user
|
||||
now = timezone.now()
|
||||
if not user.is_authenticated:
|
||||
return {}
|
||||
return {
|
||||
'now_playing_list': Scrobble.objects.filter(
|
||||
in_progress=True,
|
||||
is_paused=False,
|
||||
user=user,
|
||||
)
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -1,19 +1,29 @@
|
||||
import calendar
|
||||
import datetime
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from books.models import Book
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from music.models import Artist, Track
|
||||
from books.models import Book
|
||||
from podcasts.models import Episode
|
||||
from profiles.utils import now_user_timezone
|
||||
from scrobbles.lastfm import LastFM
|
||||
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__)
|
||||
@ -35,6 +45,24 @@ class BaseFileImportMixin(TimeStampedModel):
|
||||
def __str__(self):
|
||||
return f"Scrobble import {self.id}"
|
||||
|
||||
@property
|
||||
def human_start(self):
|
||||
start = "Unknown"
|
||||
if self.processing_started:
|
||||
start = self.processing_started.strftime('%B %d, %Y at %H:%M')
|
||||
return start
|
||||
|
||||
@property
|
||||
def import_type(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
if class_name == 'AudioscrobblerTSVImport':
|
||||
return "Audioscrobbler"
|
||||
if class_name == 'KoReaderImport':
|
||||
return "KoReader"
|
||||
if self.__class__.__name__ == 'LastFMImport':
|
||||
return "LastFM"
|
||||
return "Generic"
|
||||
|
||||
def process(self, force=False):
|
||||
logger.warning("Process not implemented")
|
||||
|
||||
@ -43,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
|
||||
|
||||
@ -99,6 +128,14 @@ class KoReaderImport(BaseFileImportMixin):
|
||||
class Meta:
|
||||
verbose_name = "KOReader Import"
|
||||
|
||||
def __str__(self):
|
||||
return f"KoReader import on {self.human_start}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'scrobbles:koreader-import-detail', kwargs={'slug': self.uuid}
|
||||
)
|
||||
|
||||
def get_path(instance, filename):
|
||||
extension = filename.split('.')[-1]
|
||||
uuid = instance.uuid
|
||||
@ -127,6 +164,14 @@ class AudioScrobblerTSVImport(BaseFileImportMixin):
|
||||
class Meta:
|
||||
verbose_name = "AudioScrobbler TSV Import"
|
||||
|
||||
def __str__(self):
|
||||
return f"Audioscrobbler import on {self.human_start}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'scrobbles:tsv-import-detail', kwargs={'slug': self.uuid}
|
||||
)
|
||||
|
||||
def get_path(instance, filename):
|
||||
extension = filename.split('.')[-1]
|
||||
uuid = instance.uuid
|
||||
@ -159,6 +204,14 @@ class LastFmImport(BaseFileImportMixin):
|
||||
class Meta:
|
||||
verbose_name = "Last.FM Import"
|
||||
|
||||
def __str__(self):
|
||||
return f"LastFM import on {self.human_start}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'scrobbles:lastfm-import-detail', kwargs={'slug': self.uuid}
|
||||
)
|
||||
|
||||
def process(self, import_all=False):
|
||||
"""Import scrobbles found on LastFM"""
|
||||
if self.processed_finished:
|
||||
@ -213,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):
|
||||
@ -269,7 +342,22 @@ class ChartRecord(TimeStampedModel):
|
||||
return period
|
||||
|
||||
def __str__(self):
|
||||
return f"#{self.rank} in {self.period} - {self.media_obj}"
|
||||
title = f"#{self.rank} in {self.period}"
|
||||
if self.day or self.week:
|
||||
title = f"#{self.rank} on {self.period}"
|
||||
return title
|
||||
|
||||
def link(self):
|
||||
get_params = f"?date={self.year}"
|
||||
if self.week:
|
||||
get_params = get_params = get_params + f"-W{self.week}"
|
||||
if self.month:
|
||||
get_params = get_params = get_params + f"-{self.month}"
|
||||
if self.day:
|
||||
get_params = get_params = get_params + f"-{self.day}"
|
||||
if self.artist:
|
||||
get_params = get_params + "&media=Artist"
|
||||
return reverse('scrobbles:charts-home') + get_params
|
||||
|
||||
@classmethod
|
||||
def build(cls, user, **kwargs):
|
||||
|
||||
@ -89,10 +89,6 @@ def mopidy_scrobble_track(
|
||||
"mopidy_status": data_dict.get("status"),
|
||||
}
|
||||
|
||||
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
|
||||
track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
|
||||
track.save()
|
||||
|
||||
scrobble = Scrobble.create_or_update(track, user_id, mopidy_data)
|
||||
|
||||
return scrobble
|
||||
@ -130,7 +126,7 @@ def jellyfin_scrobble_track(
|
||||
)
|
||||
|
||||
# Jellyfin has some race conditions with it's webhooks, these hacks fix some of them
|
||||
if not data_dict.get("PlaybackPositionTicks") or null_position_on_progress:
|
||||
if null_position_on_progress:
|
||||
logger.error("No playback position tick from Jellyfin, aborting")
|
||||
return
|
||||
|
||||
@ -152,7 +148,6 @@ def jellyfin_scrobble_track(
|
||||
)
|
||||
track = get_or_create_track(
|
||||
title=data_dict.get("Name"),
|
||||
mbid=data_dict.get(JELLYFIN_POST_KEYS["TRACK_MB_ID"]),
|
||||
artist=artist,
|
||||
album=album,
|
||||
run_time_ticks=run_time_ticks,
|
||||
|
||||
12
vrobbler/apps/scrobbles/static/css/bootstrap.min.css
vendored
Normal file
12
vrobbler/apps/scrobbles/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@ -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(
|
||||
@ -137,4 +140,97 @@ def build_charts(
|
||||
if model_str == 'Artist':
|
||||
chart_record['artist'] = result
|
||||
chart_records.append(ChartRecord(**chart_record))
|
||||
ChartRecord.objects.bulk_create(chart_records, ignore_conflicts=True)
|
||||
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,
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
0
vrobbler/apps/scrobbles/templatetags/__init__.py
Normal file
0
vrobbler/apps/scrobbles/templatetags/__init__.py
Normal file
10
vrobbler/apps/scrobbles/templatetags/urlreplace.py
Normal file
10
vrobbler/apps/scrobbles/templatetags/urlreplace.py
Normal 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()
|
||||
78
vrobbler/apps/scrobbles/theaudiodb.py
Normal file
78
vrobbler/apps/scrobbles/theaudiodb.py
Normal file
@ -0,0 +1,78 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY")
|
||||
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(ARTIST_SEARCH_URL + 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['artists']:
|
||||
artist = results['artists'][0]
|
||||
|
||||
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
|
||||
@ -42,6 +42,26 @@ urlpatterns = [
|
||||
name='mopidy-webhook',
|
||||
),
|
||||
path('export/', views.export, name='export'),
|
||||
path(
|
||||
'imports/',
|
||||
views.ScrobbleImportListView.as_view(),
|
||||
name='import-detail',
|
||||
),
|
||||
path(
|
||||
'imports/tsv/<slug:slug>/',
|
||||
views.ScrobbleTSVImportDetailView.as_view(),
|
||||
name='tsv-import-detail',
|
||||
),
|
||||
path(
|
||||
'imports/lastfm/<slug:slug>/',
|
||||
views.ScrobbleLastFMImportDetailView.as_view(),
|
||||
name='lastfm-import-detail',
|
||||
),
|
||||
path(
|
||||
'imports/koreader/<slug:slug>/',
|
||||
views.ScrobbleKoReaderImportDetailView.as_view(),
|
||||
name='koreader-import-detail',
|
||||
),
|
||||
path(
|
||||
'charts/',
|
||||
views.ChartRecordView.as_view(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import calendar
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
@ -9,19 +9,15 @@ from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.db.models.fields import timezone
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import FormView, TemplateView
|
||||
from django.views.generic import DetailView, FormView, TemplateView
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.views.generic.list import ListView
|
||||
from music.aggregators import (
|
||||
scrobble_counts,
|
||||
top_artists,
|
||||
top_tracks,
|
||||
week_of_scrobbles,
|
||||
)
|
||||
from music.aggregators import scrobble_counts, week_of_scrobbles
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import (
|
||||
api_view,
|
||||
@ -61,6 +57,8 @@ from scrobbles.tasks import (
|
||||
)
|
||||
from scrobbles.thesportsdb import lookup_event_from_thesportsdb
|
||||
|
||||
from vrobbler.apps.music.aggregators import live_charts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -70,18 +68,7 @@ class RecentScrobbleList(ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
now = timezone.now()
|
||||
if user.is_authenticated:
|
||||
if user.profile:
|
||||
timezone.activate(pytz.timezone(user.profile.timezone))
|
||||
now = timezone.localtime(timezone.now())
|
||||
data['now_playing_list'] = Scrobble.objects.filter(
|
||||
in_progress=True,
|
||||
is_paused=False,
|
||||
timestamp__lte=now,
|
||||
user=user,
|
||||
)
|
||||
|
||||
completed_for_user = Scrobble.objects.filter(
|
||||
played_to_completion=True, user=user
|
||||
)
|
||||
@ -97,16 +84,33 @@ class RecentScrobbleList(ListView):
|
||||
sport_event__isnull=False
|
||||
).order_by('-timestamp')[:15]
|
||||
|
||||
# data['top_daily_tracks'] = top_tracks()
|
||||
data['top_weekly_tracks'] = top_tracks(user, filter='week')
|
||||
data['top_monthly_tracks'] = top_tracks(user, filter='month')
|
||||
data['active_imports'] = AudioScrobblerTSVImport.objects.filter(
|
||||
processing_started__isnull=False,
|
||||
processed_finished__isnull=True,
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
# data['top_daily_artists'] = top_artists()
|
||||
data['top_weekly_artists'] = top_artists(user, filter='week')
|
||||
data['top_monthly_artists'] = top_artists(user, filter='month')
|
||||
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
|
||||
@ -118,6 +122,57 @@ class RecentScrobbleList(ListView):
|
||||
).order_by('-timestamp')[:15]
|
||||
|
||||
|
||||
class ScrobbleImportListView(TemplateView):
|
||||
template_name = "scrobbles/import_list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
context_data['object_list'] = []
|
||||
|
||||
context_data["tsv_imports"] = AudioScrobblerTSVImport.objects.filter(
|
||||
user=self.request.user,
|
||||
).order_by('-processing_started')
|
||||
context_data["koreader_imports"] = KoReaderImport.objects.filter(
|
||||
user=self.request.user,
|
||||
).order_by('-processing_started')
|
||||
context_data["lastfm_imports"] = LastFmImport.objects.filter(
|
||||
user=self.request.user,
|
||||
).order_by('-processing_started')
|
||||
return context_data
|
||||
|
||||
|
||||
class BaseScrobbleImportDetailView(DetailView):
|
||||
slug_field = 'uuid'
|
||||
template_name = "scrobbles/import_detail.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(user=self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
title = "Generic Scrobble Import"
|
||||
if self.model == KoReaderImport:
|
||||
title = "KoReader Import"
|
||||
if self.model == AudioScrobblerTSVImport:
|
||||
title = "Audioscrobbler TSV Import"
|
||||
if self.model == LastFmImport:
|
||||
title = "LastFM Import"
|
||||
context_data['title'] = title
|
||||
return context_data
|
||||
|
||||
|
||||
class ScrobbleKoReaderImportDetailView(BaseScrobbleImportDetailView):
|
||||
model = KoReaderImport
|
||||
|
||||
|
||||
class ScrobbleTSVImportDetailView(BaseScrobbleImportDetailView):
|
||||
model = AudioScrobblerTSVImport
|
||||
|
||||
|
||||
class ScrobbleLastFMImportDetailView(BaseScrobbleImportDetailView):
|
||||
model = LastFmImport
|
||||
|
||||
|
||||
class ManualScrobbleView(FormView):
|
||||
form_class = ScrobbleForm
|
||||
template_name = 'scrobbles/manual_form.html'
|
||||
@ -366,44 +421,123 @@ def export(request):
|
||||
class ChartRecordView(TemplateView):
|
||||
template_name = 'scrobbles/chart_index.html'
|
||||
|
||||
@staticmethod
|
||||
def get_media_filter(media_type: str = "") -> Q:
|
||||
filters = {
|
||||
"Track": Q(track__isnull=False),
|
||||
"Artist": Q(artist__isnull=False),
|
||||
"Series": Q(series__isnull=False),
|
||||
"Video": Q(video__isnull=False),
|
||||
"": Q(),
|
||||
}
|
||||
return filters[media_type]
|
||||
|
||||
def get_chart_records(self, media_type: str = "", **kwargs):
|
||||
media_filter = self.get_media_filter(media_type)
|
||||
charts = ChartRecord.objects.filter(
|
||||
media_filter, user=self.request.user, **kwargs
|
||||
).order_by("rank")
|
||||
|
||||
if charts.count() == 0:
|
||||
ChartRecord.build(
|
||||
user=self.request.user, model_str=media_type, **kwargs
|
||||
)
|
||||
charts = ChartRecord.objects.filter(
|
||||
media_filter, user=self.request.user, **kwargs
|
||||
).order_by("rank")
|
||||
return charts
|
||||
|
||||
def get_chart(
|
||||
self, period: str = "all_time", limit=15, media: str = ""
|
||||
) -> QuerySet:
|
||||
now = timezone.now()
|
||||
params = {}
|
||||
params['media_type'] = media
|
||||
if period == "today":
|
||||
params['day'] = now.day
|
||||
params['month'] = now.month
|
||||
params['year'] = now.year
|
||||
if period == "week":
|
||||
params['week'] = now.ioscalendar()[1]
|
||||
params['year'] = now.year
|
||||
if period == "month":
|
||||
params['month'] = now.month
|
||||
params['year'] = now.year
|
||||
if period == "year":
|
||||
params['year'] = now.year
|
||||
return self.get_chart_records(**params)[:limit]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
|
||||
date = self.request.GET.get('date')
|
||||
media_type = self.request.GET.get('media')
|
||||
date = self.request.GET.get("date")
|
||||
media_type = self.request.GET.get("media", "Track")
|
||||
user = self.request.user
|
||||
params = {}
|
||||
context_data["artist_charts"] = {}
|
||||
|
||||
if not media_type:
|
||||
media_type = 'Track'
|
||||
context_data['media_type'] = media_type
|
||||
media_filter = Q(track__isnull=False)
|
||||
if media_type == 'Video':
|
||||
media_filter = Q(video__isnull=False)
|
||||
if media_type == 'Artist':
|
||||
media_filter = Q(artist__isnull=False)
|
||||
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", 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),
|
||||
}
|
||||
|
||||
year = timezone.now().year
|
||||
track_params = {'user': user, 'media_type': 'Track'}
|
||||
context_data['current_track_charts'] = {
|
||||
"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
|
||||
|
||||
# Date provided, lookup past charts, returning nothing if it's now or in the future.
|
||||
now = timezone.now()
|
||||
year = now.year
|
||||
params = {'year': year}
|
||||
name = f"Chart for {year}"
|
||||
|
||||
if not date:
|
||||
date = timezone.now().strftime("%Y-%m-%d")
|
||||
|
||||
date_params = date.split('-')
|
||||
year = int(date_params[0])
|
||||
in_progress = False
|
||||
if len(date_params) == 2:
|
||||
if 'W' in date_params[1]:
|
||||
week = int(date_params[1].strip('W"'))
|
||||
params['week'] = week
|
||||
r = datetime.strptime(date + '-1', "%Y-W%W-%w").strftime(
|
||||
'Week of %B %d, %Y'
|
||||
start = datetime.strptime(date + "-1", "%Y-W%W-%w").replace(
|
||||
tzinfo=pytz.utc
|
||||
)
|
||||
name = f"Chart for {r}"
|
||||
end = start + timedelta(days=6)
|
||||
in_progress = start <= now <= end
|
||||
as_str = start.strftime('Week of %B %d, %Y')
|
||||
name = f"Chart for {as_str}"
|
||||
else:
|
||||
month = int(date_params[1])
|
||||
params['month'] = month
|
||||
month_str = calendar.month_name[month]
|
||||
name = f"Chart for {month_str} {year}"
|
||||
in_progress = now.month == month and now.year == year
|
||||
if len(date_params) == 3:
|
||||
month = int(date_params[1])
|
||||
day = int(date_params[2])
|
||||
@ -411,19 +545,39 @@ class ChartRecordView(TemplateView):
|
||||
params['day'] = day
|
||||
month_str = calendar.month_name[month]
|
||||
name = f"Chart for {month_str} {day}, {year}"
|
||||
in_progress = (
|
||||
now.month == month and now.year == year and now.day == day
|
||||
)
|
||||
|
||||
charts = ChartRecord.objects.filter(
|
||||
media_filter = self.get_media_filter("Track")
|
||||
track_charts = ChartRecord.objects.filter(
|
||||
media_filter, user=self.request.user, **params
|
||||
).order_by("rank")
|
||||
media_filter = self.get_media_filter("Artist")
|
||||
artist_charts = ChartRecord.objects.filter(
|
||||
media_filter, user=self.request.user, **params
|
||||
).order_by("rank")
|
||||
|
||||
if charts.count() == 0:
|
||||
if track_charts.count() == 0 and not in_progress:
|
||||
ChartRecord.build(
|
||||
user=self.request.user, model_str=media_type, **params
|
||||
user=self.request.user, model_str="Track", **params
|
||||
)
|
||||
charts = ChartRecord.objects.filter(
|
||||
media_filter = self.get_media_filter("Track")
|
||||
track_charts = ChartRecord.objects.filter(
|
||||
media_filter, user=self.request.user, **params
|
||||
).order_by("rank")
|
||||
if artist_charts.count() == 0 and not in_progress:
|
||||
ChartRecord.build(
|
||||
user=self.request.user, model_str="Artist", **params
|
||||
)
|
||||
media_filter = self.get_media_filter("Artist")
|
||||
artist_charts = ChartRecord.objects.filter(
|
||||
media_filter, user=self.request.user, **params
|
||||
).order_by("rank")
|
||||
|
||||
context_data['object_list'] = charts
|
||||
context_data['name'] = name
|
||||
context_data['media_type'] = media_type
|
||||
context_data['track_charts'] = track_charts
|
||||
context_data['artist_charts'] = artist_charts
|
||||
context_data['name'] = " ".join(["Top", media_type, "for", name])
|
||||
context_data['in_progress'] = in_progress
|
||||
return context_data
|
||||
|
||||
0
vrobbler/apps/sports/api/__init__.py
Normal file
0
vrobbler/apps/sports/api/__init__.py
Normal file
52
vrobbler/apps/sports/api/serializers.py
Normal file
52
vrobbler/apps/sports/api/serializers.py
Normal file
@ -0,0 +1,52 @@
|
||||
from rest_framework import serializers
|
||||
from sports.models import (
|
||||
League,
|
||||
SportEvent,
|
||||
Round,
|
||||
Player,
|
||||
Team,
|
||||
Season,
|
||||
Sport,
|
||||
)
|
||||
|
||||
|
||||
class SportEventSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = SportEvent
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class LeagueSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = League
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class RoundSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Round
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Player
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class TeamSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SeasonSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Season
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SportSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Sport
|
||||
fields = "__all__"
|
||||
61
vrobbler/apps/sports/api/views.py
Normal file
61
vrobbler/apps/sports/api/views.py
Normal file
@ -0,0 +1,61 @@
|
||||
from rest_framework import permissions, viewsets
|
||||
from sports.api.serializers import (
|
||||
LeagueSerializer,
|
||||
PlayerSerializer,
|
||||
RoundSerializer,
|
||||
SeasonSerializer,
|
||||
SportEventSerializer,
|
||||
SportSerializer,
|
||||
TeamSerializer,
|
||||
)
|
||||
from sports.models import (
|
||||
League,
|
||||
Player,
|
||||
Round,
|
||||
Season,
|
||||
Sport,
|
||||
SportEvent,
|
||||
Team,
|
||||
)
|
||||
|
||||
|
||||
class SportEventViewSet(viewsets.ModelViewSet):
|
||||
queryset = SportEvent.objects.all().order_by('-created')
|
||||
serializer_class = SportEventSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class LeagueViewSet(viewsets.ModelViewSet):
|
||||
queryset = League.objects.all().order_by('-created')
|
||||
serializer_class = LeagueSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class RoundViewSet(viewsets.ModelViewSet):
|
||||
queryset = Round.objects.all().order_by('-created')
|
||||
serializer_class = RoundSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class SportViewSet(viewsets.ModelViewSet):
|
||||
queryset = Sport.objects.all().order_by('-created')
|
||||
serializer_class = SportSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class PlayerViewSet(viewsets.ModelViewSet):
|
||||
queryset = Player.objects.all().order_by('-created')
|
||||
serializer_class = PlayerSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class TeamViewSet(viewsets.ModelViewSet):
|
||||
queryset = Team.objects.all().order_by('-created')
|
||||
serializer_class = TeamSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class SeasonViewSet(viewsets.ModelViewSet):
|
||||
queryset = Season.objects.all().order_by('-created')
|
||||
serializer_class = SeasonSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
18
vrobbler/apps/sports/urls.py
Normal file
18
vrobbler/apps/sports/urls.py
Normal 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',
|
||||
),
|
||||
]
|
||||
12
vrobbler/apps/sports/views.py
Normal file
12
vrobbler/apps/sports/views.py
Normal 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'
|
||||
@ -2,6 +2,7 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
import dj_database_url
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dotenv import load_dotenv
|
||||
@ -50,13 +51,9 @@ DELETE_STALE_SCROBBLES = os.getenv("VROBBLER_DELETE_STALE_SCROBBLES", True)
|
||||
# Used to dump data coming from srobbling sources, helpful for building new inputs
|
||||
DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", False)
|
||||
|
||||
|
||||
THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2")
|
||||
THESPORTSDB_BASE_URL = os.getenv(
|
||||
"VROBBLER_THESPORTSDB_BASE_URL", "https://www.thesportsdb.com/api/v1/json/"
|
||||
)
|
||||
THEAUDIODB_API_KEY = os.getenv("VROBBLER_THEAUDIODB_API_KEY", "2")
|
||||
TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
|
||||
|
||||
LASTFM_API_KEY = os.getenv("VROBBLER_LASTFM_API_KEY")
|
||||
LASTFM_SECRET_KEY = os.getenv("VROBBLER_LASTFM_SECRET_KEY")
|
||||
|
||||
@ -71,6 +68,10 @@ CSRF_TRUSTED_ORIGINS = [
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
|
||||
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
|
||||
if REDIS_URL:
|
||||
print(f"Sending tasks to redis@{REDIS_URL.split('@')[-1]}")
|
||||
else:
|
||||
print("Eagerly running all tasks")
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", False)
|
||||
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
|
||||
@ -135,6 +136,7 @@ TEMPLATES = [
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"videos.context_processors.video_lists",
|
||||
"music.context_processors.music_lists",
|
||||
"scrobbles.context_processors.now_playing",
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -150,10 +152,18 @@ DATABASES = {
|
||||
conn_max_age=600,
|
||||
),
|
||||
}
|
||||
|
||||
if TESTING:
|
||||
DATABASES = {
|
||||
"default": dj_database_url.config(default="sqlite:///testdb.sqlite3")
|
||||
}
|
||||
db_str = ""
|
||||
if 'sqlite' in DATABASES['default']['ENGINE']:
|
||||
db_str = f"Connected to sqlite@{DATABASES['default']['NAME']}"
|
||||
if 'postgresql' in DATABASES['default']['ENGINE']:
|
||||
db_str = f"Connected to postgres@{DATABASES['default']['HOST']}/{DATABASES['default']['NAME']}"
|
||||
if db_str:
|
||||
print(db_str)
|
||||
|
||||
|
||||
CACHES = {
|
||||
@ -221,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")
|
||||
|
||||
@ -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>
|
||||
@ -204,25 +205,6 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if now_playing_list and user.is_authenticated %}
|
||||
<ul style="padding-right:10px;">
|
||||
<b>Now playing</b>
|
||||
{% for scrobble in now_playing_list %}
|
||||
<div>
|
||||
{{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>
|
||||
<div class="progress-bar" style="margin-right:5px;">
|
||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||
</div>
|
||||
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
|
||||
<a href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
|
||||
</div>
|
||||
<hr/>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/">
|
||||
@ -230,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>
|
||||
@ -248,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>
|
||||
@ -260,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>
|
||||
@ -271,6 +259,40 @@
|
||||
</ul>
|
||||
{% block extra_nav %}
|
||||
{% endblock %}
|
||||
<hr/>
|
||||
|
||||
{% if now_playing_list and user.is_authenticated %}
|
||||
<ul style="padding-right:10px;">
|
||||
<b>Now playing</b>
|
||||
{% 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/>
|
||||
{% endif %}
|
||||
<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>
|
||||
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
|
||||
<a href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
|
||||
<hr />
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<hr/>
|
||||
{% endif %}
|
||||
|
||||
{% if active_imports %}
|
||||
{% for import in active_imports %}
|
||||
<ul style="padding-right:10px;">
|
||||
<li>Import in progress ({{import.processing_started|naturaltime}})</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
@ -280,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>
|
||||
|
||||
85
vrobbler/templates/music/album_detail.html
Normal file
85
vrobbler/templates/music/album_detail.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends "base_list.html" %}
|
||||
{% load mathfilters %}
|
||||
|
||||
{% block title %}{{object.name}}{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
|
||||
<div class="row">
|
||||
|
||||
{% if object.cover_image %}
|
||||
<p style="float:left; width:302px; padding:0; border: 1px solid #ccc">
|
||||
<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>
|
||||
{% if charts %}
|
||||
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||
{% endif %}
|
||||
<div class="col-md">
|
||||
<h3>Top tracks</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Count</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in object.tracks %}
|
||||
<tr>
|
||||
<td>{{rank}}#1</td>
|
||||
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
|
||||
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
|
||||
<td>{{track.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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<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">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in object.scrobbles %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp}}</td>
|
||||
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
|
||||
<td><a href="{{scrobble.track.artist.get_absolute_url}}">{{scrobble.track.artist.name}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -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 %}
|
||||
|
||||
@ -1,19 +1,35 @@
|
||||
{% extends "base_detail.html" %}
|
||||
{% extends "base_list.html" %}
|
||||
{% load mathfilters %}
|
||||
|
||||
{% block title %}{{object.name}}{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% block lists %}
|
||||
<div class="row">
|
||||
{% for album in artist.album_set.all %}
|
||||
{% if album.cover_image %}
|
||||
<p style="width:150px; float:left;"><img src="{{album.cover_image.url}}" width=150 height=150 /></p>
|
||||
|
||||
{% if object.thumbnail %}
|
||||
<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 %}
|
||||
<p style="float:left; width:302px; padding:0; border: 1px solid #ccc">
|
||||
<img src="{{object.album_set.first.cover_image.url}}" width=300 height=300 />
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
{% if charts %}
|
||||
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||
{% endif %}
|
||||
<div class="col-md">
|
||||
<h3>Top tracks</h3>
|
||||
<div class="table-responsive">
|
||||
@ -22,19 +38,21 @@
|
||||
<tr>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Album</th>
|
||||
<th scope="col">Count</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in object.tracks %}
|
||||
{% for track in tracks_ranked %}
|
||||
<tr>
|
||||
<td>{{rank}}#1</td>
|
||||
<td>{{track.title}}</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>
|
||||
@ -60,8 +78,8 @@
|
||||
{% for scrobble in object.scrobbles %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp}}</td>
|
||||
<td>{{scrobble.track.title}}</td>
|
||||
<td>{{scrobble.track.album.name}}</td>
|
||||
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
|
||||
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -1,13 +1,43 @@
|
||||
{% extends "base_detail.html" %}
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}{{object.title}}{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<h2>Last scrobbles</h2>
|
||||
{% for scrobble in object.scrobble_set.all %}
|
||||
<ul>
|
||||
<li>{{scrobble.timestamp|date:"d M Y h:m"}} - <img src="{{object.album.cover_image.url}}" width=25 height=25 /> - {{object}}</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% block lists %}
|
||||
<div class="row">
|
||||
{% if track.album.cover_image %}
|
||||
<p style="width:150px; float:left;"><img src="{{track.album.cover_image.url}}" width=150 height=150 /></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>{{object.scrobble_set.count}} scrobbles</p>
|
||||
{% if charts %}
|
||||
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||
{% endif %}
|
||||
<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">Track</th>
|
||||
<th scope="col">Album</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in object.scrobble_set.all %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp}}</td>
|
||||
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
|
||||
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album}}</a></td>
|
||||
<td><a href="{{scrobble.track.artist.get_absolute_url}}">{{scrobble.track.artist}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -1,30 +1,150 @@
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}{{name}}{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
<h2>{{name}}</h2>
|
||||
<div class="row">
|
||||
{% if artist_charts %}
|
||||
<div class="col-md">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">{{media_type}}</th>
|
||||
<th scope="col">Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for chartrecord in object_list %}
|
||||
<tr>
|
||||
<td>{{chartrecord.rank}}</td>
|
||||
<td><a href="{{chartrecord.media_obj.get_absolute_url}}">{{chartrecord.media_obj}}</a></td>
|
||||
<td>{{chartrecord.count}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="tab-content" id="artistTabContent">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for chart in artist_charts %}
|
||||
<tr>
|
||||
<td>{{chart.rank}}</td>
|
||||
<td><a href="{{chart.media_obj.get_absolute_url}}">{{chart.media_obj}}</a></td>
|
||||
<td>{{chart.count}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if track_charts %}
|
||||
<div class="col-md">
|
||||
<div class="tab-content" id="artistTabContent">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for chart in track_charts %}
|
||||
<tr>
|
||||
<td>{{chart.rank}}</td>
|
||||
<td><a href="{{chart.media_obj.get_absolute_url}}">{{chart.media_obj.title}}</a></td>
|
||||
<td>{{chart.count}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_artist_charts %}
|
||||
<div class="col-md">
|
||||
<h2>Top Artists</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.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">
|
||||
{% 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">
|
||||
{% 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}}-tab">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for artist in artists %}
|
||||
<tr>
|
||||
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
|
||||
<td>{{artist.num_scrobbles}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if current_track_charts %}
|
||||
<div class="col-md">
|
||||
<h2>Top Tracks</h2>
|
||||
|
||||
<ul class="nav nav-tabs" id="artistTab" 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">
|
||||
{% 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">
|
||||
{% 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 class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in tracks %}
|
||||
<tr>
|
||||
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
|
||||
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
|
||||
<td>{{track.num_scrobbles}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
13
vrobbler/templates/scrobbles/import_detail.html
Normal file
13
vrobbler/templates/scrobbles/import_detail.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base_detail.html" %}
|
||||
|
||||
{% block title %}{{title}}{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<p>Import started: {{object.processing_started}}</p>
|
||||
<p>Import finished: {{object.processed_finished}}</p>
|
||||
<p>Imported {{object.process_count}} scrobbles</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
47
vrobbler/templates/scrobbles/import_list.html
Normal file
47
vrobbler/templates/scrobbles/import_list.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}Scrobble Imports{% 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">Type</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Scrobbles Imported</th>
|
||||
<th scope="col">Finished</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in tsv_imports %}
|
||||
<tr>
|
||||
<td><a href="{{obj.get_absolute_url}}">{{obj.import_type}}</a></td>
|
||||
<td><a href="{{obj.get_absolute_url}}">{{obj.human_start}}</a></td>
|
||||
<td>{{obj.process_count}}</td>
|
||||
<td>{{obj.processed_finished}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for obj in lastfm_imports %}
|
||||
<tr>
|
||||
<td><a href="{{obj.get_absolute_url}}">{{obj.import_type}}</a></td>
|
||||
<td><a href="{{obj.get_absolute_url}}">{{obj.human_start}}</a></td>
|
||||
<td>{{obj.process_count}}</td>
|
||||
<td>{{obj.processed_finished}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for obj in koreader_imports %}
|
||||
<tr>
|
||||
<td><a href="{{obj.get_absolute_url}}">{{obj.import_type}}</a></td>
|
||||
<td><a href="{{obj.get_absolute_url}}">{{obj.human_start}}</a></td>
|
||||
<td>{{obj.process_count}}</td>
|
||||
<td>{{obj.processed_finished}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -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,270 +78,431 @@
|
||||
</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">
|
||||
<div class="col-md">
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="home-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#artists-week" type="button" role="tab" aria-controls="home"
|
||||
aria-selected="true">Weekly Artists</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="artist-month-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#artists-month" type="button" role="tab" aria-controls="home"
|
||||
aria-selected="true">Monthly Artists</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-week"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Weekly
|
||||
Tracks</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-month"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Monthly
|
||||
Tracks</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#latest-listened"
|
||||
type="button" role="tab" aria-controls="home" aria-selected="true">Tracks</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Videos</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-podcasted"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Podcasts</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-sports"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Sports</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="artists-week" role="tabpanel"
|
||||
aria-labelledby="artists-week-tab">
|
||||
<h2>Top artists this week</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for artist in top_weekly_artists %}
|
||||
<tr>
|
||||
<td>{{artist.num_scrobbles}}</td>
|
||||
<td>{{artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-content" id="myTabContent2">
|
||||
<div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
|
||||
aria-labelledby="latest-listened-tab">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Album</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in object_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
{% if scrobble.track.album.cover_image %}
|
||||
<td><a href="{{scrobble.track.album.get_absolute_url}}"><img src="{{scrobble.track.album.cover_image.url}}" width=25 height=25 style="border:1px solid black;" /></aa></td>
|
||||
{% else %}
|
||||
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
|
||||
{% endif %}
|
||||
<td><a href="{{scrobble.track.get_absolute_url }}">{{scrobble.track.title}}</a></td>
|
||||
<td><a href="{{scrobble.track.artist.get_absolute_url }}">{{scrobble.track.artist.name}}</aa></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show" id="tracks-week" role="tabpanel" aria-labelledby="tracks-week-tab">
|
||||
<h2>Top tracks this week</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in top_weekly_tracks %}
|
||||
<tr>
|
||||
<td>{{track.num_scrobbles}}</td>
|
||||
<td>{{track.title}}</td>
|
||||
<td>{{track.artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane fade show" id="tracks-month" role="tabpanel"
|
||||
aria-labelledby="tracks-month-tab">
|
||||
<h2>Top tracks this month</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in top_monthly_tracks %}
|
||||
<tr>
|
||||
<td>{{track.num_scrobbles}}</td>
|
||||
<td>{{track.title}}</td>
|
||||
<td>{{track.artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show " id="artists-month" role="tabpanel"
|
||||
aria-labelledby="artists-month-tab">
|
||||
<h2>Top artists this month</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for artist in top_monthly_artists %}
|
||||
<tr>
|
||||
<td>{{artist.num_scrobbles}}</td>
|
||||
<td>{{artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="home-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#latest-listened" type="button" role="tab" aria-controls="home"
|
||||
aria-selected="true">Tracks</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Videos</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#latest-podcasted" type="button" role="tab" aria-controls="profile"
|
||||
aria-selected="false">Podcasts</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-sports"
|
||||
type="button" role="tab" aria-controls="profile" aria-selected="false">Sports</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Album</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in object_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
{% if scrobble.track.album.cover_image %}
|
||||
<td><img src="{{scrobble.track.album.cover_image.url}}" width=50 height=50
|
||||
style="border:1px solid black;" /></td>
|
||||
{% else %}
|
||||
<td>{{scrobble.track.album.name}}</td>
|
||||
{% endif %}
|
||||
<td>{{scrobble.track.title}}</td>
|
||||
<td>{{scrobble.track.artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade show" id="latest-watched" role="tabpanel"
|
||||
aria-labelledby="latest-watched-tab">
|
||||
<h2>Latest watched</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Series</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in video_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{% if scrobble.video.tv_series%}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{%endif %} {{scrobble.video.title}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show" id="latest-watched" role="tabpanel"
|
||||
aria-labelledby="latest-watched-tab">
|
||||
<h2>Latest watched</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Series</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in video_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{% endif %} {{scrobble.video.title}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade show" id="latest-sports" role="tabpanel" aria-labelledby="latest-sports-tab">
|
||||
<h2>Latest Sports</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">League</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in sport_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.sport_event.title}}</td>
|
||||
<td>{{scrobble.sport_event.league.abbreviation}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show" id="latest-sports" role="tabpanel"
|
||||
aria-labelledby="latest-sports-tab">
|
||||
<h2>Latest Sports</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">League</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in sport_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.sport_event.title}}</td>
|
||||
<td>{{scrobble.sport_event.league.abbreviation}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade show" id="latest-podcasted" role="tabpanel"
|
||||
aria-labelledby="latest-podcasted-tab">
|
||||
<h2>Latest Podcasted</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Podcast</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in podcast_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.podcast_episode.title}}</td>
|
||||
<td>{{scrobble.podcast_episode.podcast}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show" id="latest-podcasted" role="tabpanel"
|
||||
aria-labelledby="latest-podcasted-tab">
|
||||
<h2>Latest Podcasted</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Podcast</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in podcast_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.podcast_episode.title}}</td>
|
||||
<td>{{scrobble.podcast_episode.podcast}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -313,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">×</span>
|
||||
<div aria-hidden="true">×</div>
|
||||
</button>
|
||||
</div>
|
||||
<form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
|
||||
@ -351,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">×</span>
|
||||
<div aria-hidden="true">×</div>
|
||||
</button>
|
||||
</div>
|
||||
<form action="{% url 'scrobbles:export' %}" method="get">
|
||||
@ -376,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 %}
|
||||
|
||||
33
vrobbler/templates/sports/sportevent_detail.html
Normal file
33
vrobbler/templates/sports/sportevent_detail.html
Normal 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 %}
|
||||
36
vrobbler/templates/sports/sportevent_list.html
Normal file
36
vrobbler/templates/sports/sportevent_list.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
|
||||
@ -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,
|
||||
@ -20,6 +19,14 @@ from vrobbler.apps.scrobbles.api.views import (
|
||||
LastFmImportViewSet,
|
||||
ScrobbleViewSet,
|
||||
)
|
||||
from vrobbler.apps.sports.api.views import (
|
||||
LeagueViewSet,
|
||||
PlayerViewSet,
|
||||
SeasonViewSet,
|
||||
SportEventViewSet,
|
||||
SportViewSet,
|
||||
TeamViewSet,
|
||||
)
|
||||
from vrobbler.apps.videos import urls as video_urls
|
||||
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
|
||||
|
||||
@ -35,6 +42,12 @@ router.register(r'series', SeriesViewSet)
|
||||
router.register(r'videos', VideoViewSet)
|
||||
router.register(r'authors', AuthorViewSet)
|
||||
router.register(r'books', BookViewSet)
|
||||
router.register(r'leagues', LeagueViewSet)
|
||||
router.register(r'sports', SportViewSet)
|
||||
router.register(r'seasons', SeasonViewSet)
|
||||
router.register(r'players', PlayerViewSet)
|
||||
router.register(r'sport-events', SportEventViewSet)
|
||||
router.register(r'teams', TeamViewSet)
|
||||
router.register(r'users', UserViewSet)
|
||||
router.register(r'user_profiles', UserProfileViewSet)
|
||||
|
||||
@ -45,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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user