Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd6e0f49b6 | |||
| 93299a1abd | |||
| a58ddebd23 | |||
| 41cdb96e94 | |||
| 5a8e828b81 | |||
| c84a3072be | |||
| 0bd7ed4463 | |||
| ee232aa103 | |||
| 7151646600 | |||
| 1d7cf965ef | |||
| 0a9279dbd4 | |||
| bf3479dbc7 | |||
| a99dca246b | |||
| f76aaf6a9c | |||
| ce1541bb2d |
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "0.11.3"
|
version = "0.11.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,7 @@ import time_machine
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from music.aggregators import (
|
from music.aggregators import live_charts, scrobble_counts, week_of_scrobbles
|
||||||
scrobble_counts,
|
|
||||||
top_artists,
|
|
||||||
top_tracks,
|
|
||||||
week_of_scrobbles,
|
|
||||||
)
|
|
||||||
from profiles.models import UserProfile
|
from profiles.models import UserProfile
|
||||||
from scrobbles.models import Scrobble
|
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):
|
def test_top_tracks_by_day(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
user = get_user_model().objects.first()
|
||||||
tops = top_tracks(user)
|
tops = live_charts(user)
|
||||||
assert tops[0].title == "Same in the End"
|
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):
|
def test_top_tracks_by_week(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
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"
|
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):
|
def test_top_tracks_by_month(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
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"
|
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):
|
def test_top_tracks_by_year(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
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"
|
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):
|
def test_top__artists_by_week(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
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"
|
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):
|
def test_top__artists_by_month(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
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"
|
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):
|
def test_top__artists_by_year(client, mopidy_track_request_data):
|
||||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||||
user = get_user_model().objects.first()
|
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"
|
assert tops[0].name == "Sublime"
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import imdb
|
|
||||||
from mock import patch
|
|
||||||
|
|
||||||
from vrobbler.apps.scrobbles.imdb import lookup_video_from_imdb
|
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):
|
def test_lookup_imdb_bad_id(caplog):
|
||||||
data = lookup_video_from_imdb('3409324')
|
data = lookup_video_from_imdb('3409324')
|
||||||
assert data is None
|
assert data is None
|
||||||
|
|||||||
@ -8,8 +8,18 @@ from scrobbles.admin import ScrobbleInline
|
|||||||
@admin.register(Album)
|
@admin.register(Album)
|
||||||
class AlbumAdmin(admin.ModelAdmin):
|
class AlbumAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
list_display = ("name", "year", "musicbrainz_id")
|
list_display = (
|
||||||
list_filter = ("year",)
|
"name",
|
||||||
|
"year",
|
||||||
|
"primary_artist",
|
||||||
|
"theaudiodb_genre",
|
||||||
|
"theaudiodb_mood",
|
||||||
|
"musicbrainz_id",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"theaudiodb_score",
|
||||||
|
"theaudiodb_genre",
|
||||||
|
)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
filter_horizontal = [
|
filter_horizontal = [
|
||||||
'artists',
|
'artists',
|
||||||
|
|||||||
@ -1,22 +1,14 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
import pytz
|
from django.apps import apps
|
||||||
from django.db.models import Count, Q, Sum
|
from django.db.models import Count, Q, QuerySet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from music.models import Artist, Track
|
from music.models import Artist, Track
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from videos.models import Video
|
from videos.models import Video
|
||||||
from vrobbler.apps.profiles.utils import now_user_timezone
|
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):
|
def scrobble_counts(user=None):
|
||||||
|
|
||||||
@ -92,39 +84,61 @@ def week_of_scrobbles(
|
|||||||
return scrobble_day_dict
|
return scrobble_day_dict
|
||||||
|
|
||||||
|
|
||||||
def top_tracks(
|
def live_charts(
|
||||||
user: "User", filter: str = "today", limit: int = 30
|
user: "User",
|
||||||
) -> List["Track"]:
|
media_type: str = "Track",
|
||||||
|
chart_period: str = "all",
|
||||||
|
limit: int = 15,
|
||||||
|
) -> QuerySet:
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
tzinfo = now.tzinfo
|
||||||
|
now = now.date()
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
now = now_user_timezone(user.profile)
|
now = now_user_timezone(user.profile)
|
||||||
|
tzinfo = now.tzinfo
|
||||||
|
|
||||||
start_of_today = datetime.combine(
|
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
|
||||||
now.date(), datetime.min.time(), now.tzinfo
|
start_day_of_week = now - timedelta(days=now.today().isoweekday() % 7)
|
||||||
)
|
start_day_of_month = now.replace(day=1)
|
||||||
starting_day_of_current_week = now.date() - timedelta(
|
start_day_of_year = now.replace(month=1, day=1)
|
||||||
days=now.today().isoweekday() % 7
|
|
||||||
)
|
media_model = apps.get_model(app_label='music', model_name=media_type)
|
||||||
starting_day_of_current_month = now.date().replace(day=1)
|
|
||||||
starting_day_of_current_year = now.date().replace(month=1, day=1)
|
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()
|
time_filter = Q()
|
||||||
if filter == "today":
|
completion_filter = Q(
|
||||||
time_filter = Q(scrobble__timestamp__gte=start_of_today)
|
scrobble__user=user, scrobble__played_to_completion=True
|
||||||
if filter == "week":
|
)
|
||||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_week)
|
user_filter = Q(scrobble__user=user)
|
||||||
if filter == "month":
|
count_field = "scrobble"
|
||||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_month)
|
|
||||||
if filter == "year":
|
if media_type == "Artist":
|
||||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_year)
|
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 (
|
return (
|
||||||
Track.objects.filter(time_filter)
|
media_model.objects.filter(user_filter, time_filter)
|
||||||
.annotate(
|
.annotate(
|
||||||
num_scrobbles=Count(
|
num_scrobbles=Count(
|
||||||
"scrobble",
|
count_field,
|
||||||
filter=Q(scrobble__played_to_completion=True),
|
filter=completion_filter,
|
||||||
distinct=True,
|
distinct=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -132,35 +146,5 @@ def top_tracks(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
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",
|
|
||||||
filter=Q(track__scrobble__played_to_completion=True),
|
|
||||||
distinct=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("-num_scrobbles")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def artist_scrobble_count(artist_id: int, filter: str = "today") -> int:
|
def artist_scrobble_count(artist_id: int, filter: str = "today") -> int:
|
||||||
return Scrobble.objects.filter(track__artist=artist_id).count()
|
return Scrobble.objects.filter(track__artist=artist_id).count()
|
||||||
|
|||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import ScrobblableMixin
|
||||||
from scrobbles.theaudiodb import lookup_artist_from_tadb
|
from scrobbles.theaudiodb import lookup_artist_from_tadb
|
||||||
|
from vrobbler.apps.scrobbles.theaudiodb import lookup_album_from_tadb
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
@ -86,14 +87,56 @@ class Album(TimeStampedModel):
|
|||||||
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
|
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
|
||||||
musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
|
musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
|
||||||
cover_image = models.ImageField(upload_to="albums/", **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):
|
def __str__(self):
|
||||||
return self.name
|
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
|
@property
|
||||||
def primary_artist(self):
|
def primary_artist(self):
|
||||||
return self.artists.first()
|
return self.artists.first()
|
||||||
|
|
||||||
|
def scrape_theaudiodb(self) -> None:
|
||||||
|
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):
|
def fix_metadata(self):
|
||||||
if (
|
if (
|
||||||
not self.musicbrainz_albumartist_id
|
not self.musicbrainz_albumartist_id
|
||||||
@ -141,6 +184,7 @@ class Album(TimeStampedModel):
|
|||||||
or self.cover_image == 'default-image-replace-me'
|
or self.cover_image == 'default-image-replace-me'
|
||||||
):
|
):
|
||||||
self.fetch_artwork()
|
self.fetch_artwork()
|
||||||
|
self.scrape_theaudiodb()
|
||||||
|
|
||||||
def fetch_artwork(self, force=False):
|
def fetch_artwork(self, force=False):
|
||||||
if not self.cover_image and not force:
|
if not self.cover_image and not force:
|
||||||
@ -179,9 +223,27 @@ class Album(TimeStampedModel):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mb_link(self):
|
def mb_link(self) -> str:
|
||||||
return f"https://musicbrainz.org/release/{self.musicbrainz_id}"
|
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):
|
class Track(ScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)
|
COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)
|
||||||
|
|||||||
@ -6,6 +6,11 @@ app_name = 'music'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('albums/', views.AlbumListView.as_view(), name='albums_list'),
|
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/", views.TrackListView.as_view(), name='tracks_list'),
|
||||||
path(
|
path(
|
||||||
'tracks/<slug:slug>/',
|
'tracks/<slug:slug>/',
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
from django.db.models import Count
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
from django.views import generic
|
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
|
from scrobbles.stats import get_scrobble_count_qs
|
||||||
|
|
||||||
|
|
||||||
class TrackListView(generic.ListView):
|
class TrackListView(generic.ListView):
|
||||||
model = Track
|
model = Track
|
||||||
|
paginate_by = 200
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return get_scrobble_count_qs(user=self.request.user).order_by(
|
return get_scrobble_count_qs(user=self.request.user).order_by(
|
||||||
@ -17,18 +20,64 @@ class TrackDetailView(generic.DetailView):
|
|||||||
model = Track
|
model = Track
|
||||||
slug_field = 'uuid'
|
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):
|
class ArtistListView(generic.ListView):
|
||||||
model = Artist
|
model = Artist
|
||||||
|
paginate_by = 100
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().order_by("name")
|
return super().get_queryset().order_by("name")
|
||||||
|
|
||||||
|
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):
|
class ArtistDetailView(generic.DetailView):
|
||||||
model = Artist
|
model = Artist
|
||||||
slug_field = 'uuid'
|
slug_field = 'uuid'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context_data = super().get_context_data(**kwargs)
|
||||||
|
context_data['charts'] = ChartRecord.objects.filter(
|
||||||
|
artist=self.object, rank__in=[1, 2, 3]
|
||||||
|
)
|
||||||
|
return context_data
|
||||||
|
|
||||||
|
|
||||||
class AlbumListView(generic.ListView):
|
class AlbumListView(generic.ListView):
|
||||||
model = Album
|
model = Album
|
||||||
|
paginate_by = 50
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().order_by("name")
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|||||||
@ -312,7 +312,22 @@ class ChartRecord(TimeStampedModel):
|
|||||||
return period
|
return period
|
||||||
|
|
||||||
def __str__(self):
|
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
|
@classmethod
|
||||||
def build(cls, user, **kwargs):
|
def build(cls, user, **kwargs):
|
||||||
|
|||||||
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 |
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()
|
||||||
@ -5,14 +5,15 @@ import requests
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY")
|
THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY")
|
||||||
SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
|
ARTIST_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
|
||||||
|
ALBUM_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/searchalbum.php?s="
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def lookup_artist_from_tadb(name: str) -> dict:
|
def lookup_artist_from_tadb(name: str) -> dict:
|
||||||
artist_info = {}
|
artist_info = {}
|
||||||
response = requests.get(SEARCH_URL + name)
|
response = requests.get(ARTIST_SEARCH_URL + name)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.warn(f"Bad response from TADB: {response.status_code}")
|
logger.warn(f"Bad response from TADB: {response.status_code}")
|
||||||
@ -23,11 +24,52 @@ def lookup_artist_from_tadb(name: str) -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
results = json.loads(response.content)
|
results = json.loads(response.content)
|
||||||
artist = results['artists'][0]
|
if results['artists']:
|
||||||
|
artist = results['artists'][0]
|
||||||
|
|
||||||
artist_info['biography'] = artist['strBiographyEN']
|
artist_info['biography'] = artist.get('strBiographyEN')
|
||||||
artist_info['genre'] = artist['strGenre']
|
artist_info['genre'] = artist.get('strGenre')
|
||||||
artist_info['mood'] = artist['strMood']
|
artist_info['mood'] = artist.get('strMood')
|
||||||
artist_info['thumb_url'] = artist['strArtistThumb']
|
artist_info['thumb_url'] = artist.get('strArtistThumb')
|
||||||
|
|
||||||
return artist_info
|
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['theaudiodb_year_released'] = album.get('intYearReleased')
|
||||||
|
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('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
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import calendar
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -10,6 +9,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.fields import timezone
|
from django.db.models.fields import timezone
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -17,12 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from django.views.generic import DetailView, FormView, TemplateView
|
from django.views.generic import DetailView, FormView, TemplateView
|
||||||
from django.views.generic.edit import CreateView
|
from django.views.generic.edit import CreateView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from music.aggregators import (
|
from music.aggregators import scrobble_counts, week_of_scrobbles
|
||||||
scrobble_counts,
|
|
||||||
top_artists,
|
|
||||||
top_tracks,
|
|
||||||
week_of_scrobbles,
|
|
||||||
)
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import (
|
from rest_framework.decorators import (
|
||||||
api_view,
|
api_view,
|
||||||
@ -62,6 +57,8 @@ from scrobbles.tasks import (
|
|||||||
)
|
)
|
||||||
from scrobbles.thesportsdb import lookup_event_from_thesportsdb
|
from scrobbles.thesportsdb import lookup_event_from_thesportsdb
|
||||||
|
|
||||||
|
from vrobbler.apps.music.aggregators import live_charts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -406,19 +403,17 @@ class ChartRecordView(TemplateView):
|
|||||||
template_name = 'scrobbles/chart_index.html'
|
template_name = 'scrobbles/chart_index.html'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_media_filter(media_type: str = "Track"):
|
def get_media_filter(media_type: str = "") -> Q:
|
||||||
media_filter = Q()
|
filters = {
|
||||||
if media_type == 'Track':
|
"Track": Q(track__isnull=False),
|
||||||
media_filter = Q(track__isnull=False)
|
"Artist": Q(artist__isnull=False),
|
||||||
if media_type == 'Artist':
|
"Series": Q(series__isnull=False),
|
||||||
media_filter = Q(artist__isnull=False)
|
"Video": Q(video__isnull=False),
|
||||||
if media_type == 'Series':
|
"": Q(),
|
||||||
media_filter = Q(series__isnull=False)
|
}
|
||||||
if media_type == 'Video':
|
return filters[media_type]
|
||||||
media_filter = Q(video__isnull=False)
|
|
||||||
return media_filter
|
|
||||||
|
|
||||||
def get_chart_records(self, media_type: str = "Track", **kwargs):
|
def get_chart_records(self, media_type: str = "", **kwargs):
|
||||||
media_filter = self.get_media_filter(media_type)
|
media_filter = self.get_media_filter(media_type)
|
||||||
charts = ChartRecord.objects.filter(
|
charts = ChartRecord.objects.filter(
|
||||||
media_filter, user=self.request.user, **kwargs
|
media_filter, user=self.request.user, **kwargs
|
||||||
@ -434,37 +429,24 @@ class ChartRecordView(TemplateView):
|
|||||||
return charts
|
return charts
|
||||||
|
|
||||||
def get_chart(
|
def get_chart(
|
||||||
self, period: str = "all_time", limit=15, media: str = "Track"
|
self, period: str = "all_time", limit=15, media: str = ""
|
||||||
) -> QuerySet:
|
) -> QuerySet:
|
||||||
chart = QuerySet()
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
if period == "all_time":
|
params = {}
|
||||||
chart = self.get_chart_records(media_type=media)
|
params['media_type'] = media
|
||||||
if period == "today":
|
if period == "today":
|
||||||
chart = self.get_chart_records(
|
params['day'] = now.day
|
||||||
media_type=media,
|
params['month'] = now.month
|
||||||
day=now.day,
|
params['year'] = now.year
|
||||||
month=now.month,
|
|
||||||
year=now.year,
|
|
||||||
)
|
|
||||||
if period == "week":
|
if period == "week":
|
||||||
chart = self.get_chart_records(
|
params['week'] = now.ioscalendar()[1]
|
||||||
media_type=media,
|
params['year'] = now.year
|
||||||
year=now.year,
|
|
||||||
week=now.isocalendar()[1],
|
|
||||||
)
|
|
||||||
if period == "month":
|
if period == "month":
|
||||||
chart = self.get_chart_records(
|
params['month'] = now.month
|
||||||
media_type=media,
|
params['year'] = now.year
|
||||||
year=now.year,
|
|
||||||
month=now.month,
|
|
||||||
)
|
|
||||||
if period == "year":
|
if period == "year":
|
||||||
chart = self.get_chart_records(
|
params['year'] = now.year
|
||||||
media_type=media,
|
return self.get_chart_records(**params)[:limit]
|
||||||
year=now.year,
|
|
||||||
)
|
|
||||||
return chart[:limit]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context_data = super().get_context_data(**kwargs)
|
context_data = super().get_context_data(**kwargs)
|
||||||
@ -475,21 +457,24 @@ class ChartRecordView(TemplateView):
|
|||||||
context_data["artist_charts"] = {}
|
context_data["artist_charts"] = {}
|
||||||
|
|
||||||
if not date:
|
if not date:
|
||||||
context_data['artist_charts'] = {
|
artist_params = {'user': user, 'media_type': 'Artist'}
|
||||||
"today": top_artists(user, filter="today")[:30],
|
context_data['current_artist_charts'] = {
|
||||||
"week": top_artists(user, filter="week")[:30],
|
"today": live_charts(**artist_params, chart_period="today"),
|
||||||
"month": top_artists(user, filter="month")[:30],
|
"week": live_charts(**artist_params, chart_period="week"),
|
||||||
"all": top_artists(user),
|
"month": live_charts(**artist_params, chart_period="month"),
|
||||||
|
"all": live_charts(**artist_params),
|
||||||
}
|
}
|
||||||
|
|
||||||
context_data['track_charts'] = {
|
track_params = {'user': user, 'media_type': 'Track'}
|
||||||
"today": top_tracks(user, filter="today")[:30],
|
context_data['current_track_charts'] = {
|
||||||
"week": top_tracks(user, filter="week")[:30],
|
"today": live_charts(**track_params, chart_period="today"),
|
||||||
"month": top_tracks(user, filter="month")[:30],
|
"week": live_charts(**track_params, chart_period="week"),
|
||||||
"all": top_tracks(user),
|
"month": live_charts(**track_params, chart_period="month"),
|
||||||
|
"all": live_charts(**track_params),
|
||||||
}
|
}
|
||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
|
# Date provided, lookup past charts, returning nothing if it's now or in the future.
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
year = now.year
|
year = now.year
|
||||||
params = {'year': year}
|
params = {'year': year}
|
||||||
@ -526,24 +511,35 @@ class ChartRecordView(TemplateView):
|
|||||||
now.month == month and now.year == year and now.day == day
|
now.month == month and now.year == year and now.day == day
|
||||||
)
|
)
|
||||||
|
|
||||||
media_filter = self.get_media_filter(media_type)
|
media_filter = self.get_media_filter("Track")
|
||||||
charts = ChartRecord.objects.filter(
|
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
|
media_filter, user=self.request.user, **params
|
||||||
).order_by("rank")
|
).order_by("rank")
|
||||||
|
|
||||||
if charts.count() == 0:
|
if track_charts.count() == 0 and not in_progress:
|
||||||
ChartRecord.build(
|
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
|
media_filter, user=self.request.user, **params
|
||||||
).order_by("rank")
|
).order_by("rank")
|
||||||
|
|
||||||
if in_progress:
|
context_data['media_type'] = media_type
|
||||||
# TODO recalculate
|
context_data['track_charts'] = track_charts
|
||||||
...
|
context_data['artist_charts'] = artist_charts
|
||||||
|
context_data['name'] = " ".join(["Top", media_type, "for", name])
|
||||||
context_data['charts'] = charts
|
|
||||||
context_data['name'] = name
|
|
||||||
context_data['in_progress'] = in_progress
|
context_data['in_progress'] = in_progress
|
||||||
return context_data
|
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]
|
||||||
@ -231,11 +231,10 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = os.getenv(
|
STATIC_ROOT = os.getenv(
|
||||||
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
|
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_URL = "/media/"
|
MEDIA_URL = "/media/"
|
||||||
MEDIA_ROOT = os.getenv(
|
MEDIA_ROOT = os.getenv(
|
||||||
"VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media")
|
"VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media")
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
|
<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="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/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://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>
|
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
|
||||||
@ -211,6 +212,7 @@
|
|||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" aria-current="page" href="/charts/">
|
<a class="nav-link" aria-current="page" href="/charts/">
|
||||||
<span data-feather="bar-chart"></span>
|
<span data-feather="bar-chart"></span>
|
||||||
@ -229,6 +231,12 @@
|
|||||||
Artists
|
Artists
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/albums/">
|
||||||
|
<span data-feather="music"></span>
|
||||||
|
Albums
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/movies/">
|
<a class="nav-link" href="/movies/">
|
||||||
<span data-feather="film"></span>
|
<span data-feather="film"></span>
|
||||||
@ -241,7 +249,6 @@
|
|||||||
TV Shows
|
TV Shows
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/admin/">
|
<a class="nav-link" href="/admin/">
|
||||||
<span data-feather="key"></span>
|
<span data-feather="key"></span>
|
||||||
@ -260,13 +267,13 @@
|
|||||||
{% for scrobble in now_playing_list %}
|
{% for scrobble in now_playing_list %}
|
||||||
<div>
|
<div>
|
||||||
{% if scrobble.media_obj.album.cover_image %}
|
{% if scrobble.media_obj.album.cover_image %}
|
||||||
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120
|
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120 style="border:1px solid black; " /></td><br/>
|
||||||
style="border:1px solid black; " /></td><br/>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{scrobble.media_obj.title}}<br/>
|
<a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a><br/>
|
||||||
{% if scrobble.media_obj.subtitle %}<em>{{scrobble.media_obj.subtitle}}</em><br/>{% endif %}
|
{% if scrobble.media_obj.subtitle %}
|
||||||
<small>{{scrobble.timestamp|naturaltime}}<br/>
|
<em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em><br/>
|
||||||
from {{scrobble.source}}</small>
|
{% endif %}
|
||||||
|
<small>{{scrobble.timestamp|naturaltime}}<br/> from {{scrobble.source}}</small>
|
||||||
<div class="progress-bar" style="margin-right:5px;">
|
<div class="progress-bar" style="margin-right:5px;">
|
||||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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" %}
|
{% extends "base_list.html" %}
|
||||||
|
{% load urlreplace %}
|
||||||
|
|
||||||
{% block title %}Albums{% endblock %}
|
{% block title %}Albums{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
{% for album in object_list %}
|
|
||||||
<dl style="width: 130px; float: left; margin-right:10px;">
|
<div class="row">
|
||||||
<dd><img src="{{album.cover_image.url}}" width=120 height=120 /></dd>
|
<p class="view">
|
||||||
</dl>
|
<span class="view-links">
|
||||||
{% endfor %}
|
{% 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">Album</th>
|
||||||
|
<th scope="col">Artist</th>
|
||||||
|
<th scope="col">Scrobbles</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for album in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{album.get_absolute_url}}">{{album}}</a></td>
|
||||||
|
<td><a href="{{album.artist.get_absolute_url}}">{{album.artist}}</a></td>
|
||||||
|
<td>{{album.scrobbles.count}}</td>
|
||||||
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,16 +4,32 @@
|
|||||||
{% block title %}{{object.name}}{% endblock %}
|
{% block title %}{{object.name}}{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for album in artist.album_set.all %}
|
|
||||||
{% if album.cover_image %}
|
{% if object.thumbnail %}
|
||||||
<p style="width:150px; float:left;"><img src="{{album.cover_image.url}}" width=150 height=150 /></p>
|
<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 %}
|
{% 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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>{{artist.scrobbles.count}} scrobbles</p>
|
<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">
|
<div class="col-md">
|
||||||
<h3>Top tracks</h3>
|
<h3>Top tracks</h3>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -22,6 +38,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Rank</th>
|
<th scope="col">Rank</th>
|
||||||
<th scope="col">Track</th>
|
<th scope="col">Track</th>
|
||||||
|
<th scope="col">Album</th>
|
||||||
<th scope="col">Count</th>
|
<th scope="col">Count</th>
|
||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -30,7 +47,8 @@
|
|||||||
{% for track in object.tracks %}
|
{% for track in object.tracks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{rank}}#1</td>
|
<td>{{rank}}#1</td>
|
||||||
<td>{{track.title}}</td>
|
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
|
||||||
|
<td><a href="{{track.album.get_absolute_url}}">{{track.album}}</a></td>
|
||||||
<td>{{track.scrobble_count}}</td>
|
<td>{{track.scrobble_count}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="progress-bar" style="margin-right:5px;">
|
<div class="progress-bar" style="margin-right:5px;">
|
||||||
@ -60,8 +78,8 @@
|
|||||||
{% for scrobble in object.scrobbles %}
|
{% for scrobble in object.scrobbles %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{scrobble.timestamp}}</td>
|
<td>{{scrobble.timestamp}}</td>
|
||||||
<td>{{scrobble.track.title}}</td>
|
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
|
||||||
<td>{{scrobble.track.album.name}}</td>
|
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -1,9 +1,45 @@
|
|||||||
{% extends "base_list.html" %}
|
{% extends "base_list.html" %}
|
||||||
|
{% load urlreplace %}
|
||||||
|
|
||||||
{% block title %}Artists{% endblock %}
|
{% block title %}Artists{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
<div class="row">
|
<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="col-md">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
@ -26,5 +62,20 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,13 +1,43 @@
|
|||||||
{% extends "base_detail.html" %}
|
{% extends "base_list.html" %}
|
||||||
|
|
||||||
{% block title %}{{object.title}}{% endblock %}
|
{% block title %}{{object.title}}{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block lists %}
|
||||||
<h2>Last scrobbles</h2>
|
<div class="row">
|
||||||
{% for scrobble in object.scrobble_set.all %}
|
{% if track.album.cover_image %}
|
||||||
<ul>
|
<p style="width:150px; float:left;"><img src="{{track.album.cover_image.url}}" width=150 height=150 /></p>
|
||||||
<li>{{scrobble.timestamp|date:"d M Y h:m"}} - <img src="{{object.album.cover_image.url}}" width=25 height=25 /> - {{object}}</li>
|
{% endif %}
|
||||||
</ul>
|
</div>
|
||||||
{% endfor %}
|
<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 %}
|
{% endblock %}
|
||||||
|
|||||||
@ -3,10 +3,57 @@
|
|||||||
{% block title %}Tracks{% endblock %}
|
{% block title %}Tracks{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
<h2>All time</h2>
|
<div class="row">
|
||||||
{% for track in object_list %}
|
<p class="pagination">
|
||||||
<ul>
|
<span class="page-links">
|
||||||
<li><a href="{{track.get_absolute_url}}">{{track}}</a></li>
|
{% if page_obj.has_previous %}
|
||||||
</ul>
|
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||||
{% endfor %}
|
{% 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">Track</th>
|
||||||
|
<th scope="col">Artist</th>
|
||||||
|
<th scope="col">Scrobbles</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for track in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{track.get_absolute_url}}">{{track}}</a></td>
|
||||||
|
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
|
||||||
|
<td>{{track.scrobble_set.count}}</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,118 +4,145 @@
|
|||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if charts %}
|
|
||||||
<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">Track</th>
|
|
||||||
<th scope="col">Scrobbles</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for chart in charts %}
|
|
||||||
<tr>
|
|
||||||
<td>{{chart.rank}}</td>
|
|
||||||
<td><a href="{{chart.media_obj.artist.get_absolute_url}}">{{chart.media_obj.artist}}</a></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>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if artist_charts %}
|
{% if artist_charts %}
|
||||||
<h2>Top Artists</h2>
|
<div class="col-md">
|
||||||
|
<div class="tab-content" id="artistTabContent">
|
||||||
<ul class="nav nav-tabs" id="artistTab" role="tablist">
|
|
||||||
{% for chart_name in artist_charts.keys %}
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link {% if forloop.first %}active{% endif %}" id="artist-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{chart_name}}"
|
|
||||||
type="button" role="tab" aria-controls="home" aria-selected="true">
|
|
||||||
{{chart_name}}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content" id="artistTabContent">
|
|
||||||
{% for chart_name, artists in 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>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
{% if track_charts %}
|
|
||||||
<h2>Top Tracks</h2>
|
|
||||||
|
|
||||||
<ul class="nav nav-tabs" id="artistTab" role="tablist">
|
|
||||||
{% for chart_name in track_charts.keys %}
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link {% if forloop.first %}active{% endif %}" id="track-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#track-{{chart_name}}"
|
|
||||||
type="button" role="tab" aria-controls="home" aria-selected="true">
|
|
||||||
{{chart_name}}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content" id="trackTabContent">
|
|
||||||
{% for chart_name, tracks in 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">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Rank</th>
|
<th scope="col">Rank</th>
|
||||||
<th scope="col">Artist</th>
|
<th scope="col">Artist</th>
|
||||||
<th scope="col">Track</th>
|
|
||||||
<th scope="col">Scrobbles</th>
|
<th scope="col">Scrobbles</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for track in tracks %}
|
{% for chart in artist_charts %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{track.rank}}</td>
|
<td>{{chart.rank}}</td>
|
||||||
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
|
<td><a href="{{chart.media_obj.get_absolute_url}}">{{chart.media_obj}}</a></td>
|
||||||
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
|
<td>{{chart.count}}</td>
|
||||||
<td>{{track.num_scrobbles}}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</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">
|
||||||
|
{{chart_name}}
|
||||||
|
</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">
|
||||||
|
{{chart_name}}
|
||||||
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -84,13 +84,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||||
{% if scrobble.track.album.cover_image %}
|
{% if scrobble.track.album.cover_image %}
|
||||||
<td><img src="{{scrobble.track.album.cover_image.url}}" width=25 height=25
|
<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>
|
||||||
style="border:1px solid black;" /></td>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{scrobble.track.album.name}}</td>
|
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{scrobble.track.title}}</td>
|
<td><a href="{{scrobble.track.get_absolute_url }}">{{scrobble.track.title}}</a></td>
|
||||||
<td>{{scrobble.track.artist.name}}</td>
|
<td><a href="{{scrobble.track.artist.get_absolute_url }}">{{scrobble.track.artist.name}}</aa></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
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.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework import routers
|
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.books.api.views import AuthorViewSet, BookViewSet
|
||||||
from vrobbler.apps.music import urls as music_urls
|
from vrobbler.apps.music import urls as music_urls
|
||||||
from vrobbler.apps.music.api.views import (
|
from vrobbler.apps.music.api.views import (
|
||||||
@ -20,6 +18,14 @@ from vrobbler.apps.scrobbles.api.views import (
|
|||||||
LastFmImportViewSet,
|
LastFmImportViewSet,
|
||||||
ScrobbleViewSet,
|
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 import urls as video_urls
|
||||||
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
|
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
|
||||||
|
|
||||||
@ -35,6 +41,12 @@ router.register(r'series', SeriesViewSet)
|
|||||||
router.register(r'videos', VideoViewSet)
|
router.register(r'videos', VideoViewSet)
|
||||||
router.register(r'authors', AuthorViewSet)
|
router.register(r'authors', AuthorViewSet)
|
||||||
router.register(r'books', BookViewSet)
|
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'users', UserViewSet)
|
||||||
router.register(r'user_profiles', UserProfileViewSet)
|
router.register(r'user_profiles', UserProfileViewSet)
|
||||||
|
|
||||||
@ -50,11 +62,3 @@ urlpatterns = [
|
|||||||
"", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"
|
"", 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