Compare commits

...

7 Commits
0.2.0 ... 0.3.0

13 changed files with 299 additions and 158 deletions

View File

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

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from music.models import Artist, Album, Track
@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
date_hierarchy = "created"
@ -9,12 +10,14 @@ class AlbumAdmin(admin.ModelAdmin):
list_filter = ("year",)
ordering = ("name",)
@admin.register(Artist)
class ArtistAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = ("name", "musicbrainz_id")
ordering = ("name",)
@admin.register(Track)
class TrackAdmin(admin.ModelAdmin):
date_hierarchy = "created"

View File

@ -10,7 +10,9 @@ from datetime import datetime, timedelta
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_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)
@ -18,30 +20,48 @@ STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
def scrobble_counts():
finished_scrobbles_qs = Scrobble.objects.filter(in_progress=False)
data = {}
data['today'] = finished_scrobbles_qs.filter(timestamp__gte=START_OF_TODAY).count()
data['week'] = finished_scrobbles_qs.filter(timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK).count()
data['month'] = finished_scrobbles_qs.filter(timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH).count()
data['year'] = finished_scrobbles_qs.filter(timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR).count()
data['today'] = finished_scrobbles_qs.filter(
timestamp__gte=START_OF_TODAY
).count()
data['week'] = finished_scrobbles_qs.filter(
timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
).count()
data['month'] = finished_scrobbles_qs.filter(
timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
).count()
data['year'] = finished_scrobbles_qs.filter(
timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
).count()
data['alltime'] = finished_scrobbles_qs.count()
return data
def week_of_scrobbles(media: str='tracks') -> dict[str, int]:
scrobble_day_dict= {}
media_filter = Q(track__isnull=True)
for day in range(1,8):
def week_of_scrobbles(media: str = 'tracks') -> dict[str, int]:
scrobble_day_dict = {}
media_filter = Q(track__isnull=False)
for day in range(6, -1, -1):
start = START_OF_TODAY - timedelta(days=day)
end = datetime.combine(start, datetime.max.time(), NOW.tzinfo)
day_of_week = start.strftime('%A')
if media == 'movies':
media_filter = Q(video__videotype=Video.VideoType.MOVIE)
if media == 'series':
media_filter = Q(video__videotype=Video.VideoType.MOVIE)
scrobble_day_dict[day_of_week] = Scrobble.objects.filter(media_filter).filter(timestamp__lte=START_OF_TODAY, timestamp__gt=end, in_progress=False).count()
media_filter = Q(video__videotype=Video.VideoType.TV_EPISODE)
scrobble_day_dict[day_of_week] = (
Scrobble.objects.filter(media_filter)
.filter(
timestamp__gte=start,
timestamp__lte=end,
in_progress=False,
)
.count()
)
return scrobble_day_dict
def top_tracks(filter: str="today", limit: int=15) -> List["Track"]:
def top_tracks(filter: str = "today", limit: int = 15) -> List["Track"]:
time_filter = Q(scrobble__timestamp__gte=START_OF_TODAY)
if filter == "week":
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK)
@ -50,21 +70,36 @@ def top_tracks(filter: str="today", limit: int=15) -> List["Track"]:
if filter == "year":
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR)
return Track.objects.annotate(num_scrobbles=Count("scrobble", distinct=True)).filter(time_filter).order_by("-num_scrobbles")[:limit]
return (
Track.objects.annotate(num_scrobbles=Count("scrobble", distinct=True))
.filter(time_filter)
.order_by("-num_scrobbles")[:limit]
)
def top_artists(filter: str="today", limit: int=15) -> List["Artist"]:
def top_artists(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)
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)
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)
time_filter = Q(
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
)
return (
Artist.objects.annotate(
num_scrobbles=Count("track__scrobble", distinct=True)
)
.filter(time_filter)
.order_by("-num_scrobbles")[:limit]
)
return Artist.objects.annotate(num_scrobbles=Sum("track__scrobble", distinct=True)).filter(time_filter).order_by("-num_scrobbles")[:limit]
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()

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.5 on 2023-01-11 03:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0003_album_uuid_artist_uuid_track_uuid'),
]
operations = [
migrations.AlterModelOptions(
name='artist',
options={},
),
migrations.AlterField(
model_name='album',
name='musicbrainz_id',
field=models.CharField(
blank=True, max_length=255, null=True, unique=True
),
),
migrations.AlterField(
model_name='track',
name='musicbrainz_id',
field=models.CharField(
blank=True, max_length=255, null=True, unique=True
),
),
migrations.AlterUniqueTogether(
name='artist',
unique_together={('name', 'musicbrainz_id')},
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 4.1.5 on 2023-01-09 16:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0003_album_uuid_artist_uuid_track_uuid'),
]
operations = [
migrations.AddField(
model_name='track',
name='thumbs',
field=models.IntegerField(
choices=[
(-1, 'Thumbs down'),
(0, 'No opinion'),
(1, 'Thumbs up'),
],
default=0,
),
),
]

View File

@ -16,7 +16,7 @@ class Album(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
name = models.CharField(max_length=255)
year = models.IntegerField(**BNULL)
musicbrainz_id = models.CharField(max_length=255, **BNULL)
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
@ -33,6 +33,9 @@ class Artist(TimeStampedModel):
name = models.CharField(max_length=255)
musicbrainz_id = models.CharField(max_length=255, **BNULL)
class Meta:
unique_together=[['name', 'musicbrainz_id']]
def __str__(self):
return self.name
@ -51,10 +54,10 @@ class Track(TimeStampedModel):
title = models.CharField(max_length=255, **BNULL)
artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
musicbrainz_id = models.CharField(max_length=255, **BNULL)
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
run_time = models.CharField(max_length=8, **BNULL)
run_time_ticks = models.PositiveBigIntegerField(**BNULL)
thumbs = models.IntegerField(default=Opinion.NEUTRAL, choices=Opinion.choices)
# thumbs = models.IntegerField(default=Opinion.NEUTRAL, choices=Opinion.choices)
def __str__(self):
return f"{self.title} by {self.artist}"

View File

@ -13,7 +13,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"playback_position",
"in_progress",
)
list_filter = ("in_progress", "source")
list_filter = ("in_progress", "source", "track__artist")
ordering = ("-timestamp",)

View File

@ -21,7 +21,12 @@ from scrobbles.models import Scrobble
from scrobbles.serializers import ScrobbleSerializer
from scrobbles.utils import convert_to_seconds
from videos.models import Video
from vrobbler.apps.music.aggregators import scrobble_counts, top_tracks, week_of_scrobbles
from vrobbler.apps.music.aggregators import (
scrobble_counts,
top_artists,
top_tracks,
week_of_scrobbles,
)
logger = logging.getLogger(__name__)
@ -52,18 +57,25 @@ class RecentScrobbleList(ListView):
timestamp__gte=last_eight_minutes,
timestamp__lte=now,
)
data['video_scrobble_list'] = Scrobble.objects.filter(video__isnull=False, in_progress=False).order_by('-timestamp')[:10]
data['video_scrobble_list'] = Scrobble.objects.filter(
video__isnull=False, in_progress=False
).order_by('-timestamp')[:10]
data['top_daily_tracks'] = top_tracks()
data['top_weekly_tracks'] = top_tracks(filter='week')
data['top_monthly_tracks'] = top_tracks(filter='month')
data['top_daily_artists'] = top_artists()
data['top_weekly_artists'] = top_artists(filter='week')
data['top_monthly_artists'] = top_artists(filter='month')
data["weekly_data"] = week_of_scrobbles()
data['counts'] = scrobble_counts()
return data
def get_queryset(self):
return Scrobble.objects.filter(track__isnull=False, in_progress=False).order_by(
'-timestamp'
)[:25]
return Scrobble.objects.filter(
track__isnull=False, in_progress=False
).order_by('-timestamp')[:15]
@csrf_exempt

View File

@ -0,0 +1,21 @@
# Generated by Django 4.1.5 on 2023-01-11 03:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('videos', '0004_series_uuid_video_uuid'),
]
operations = [
migrations.AlterModelOptions(
name='video',
options={},
),
migrations.AlterUniqueTogether(
name='video',
unique_together={('title', 'imdb_id')},
),
]

View File

@ -58,7 +58,8 @@ class Video(TimeStampedModel):
imdb_id = models.CharField(max_length=20, **BNULL)
tvrage_id = models.CharField(max_length=20, **BNULL)
# Metadata fields from TMDB
class Meta:
unique_together = [['title', 'imdb_id']]
def __str__(self):
if self.video_type == self.VideoType.TV_EPISODE:
@ -107,6 +108,7 @@ class Video(TimeStampedModel):
video_dict["season_number"] = data_dict.get("SeasonNumber", "")
video, created = cls.objects.get_or_create(**video_dict)
if created:
logger.debug(f"Created new video: {video}")
else:

View File

@ -6,15 +6,18 @@ from os import environ as env
if not 'DJANGO_SETTINGS_MODULE' in env:
from vrobbler import settings
env.setdefault('DJANGO_SETTINGS_MODULE', settings.__name__)
import django
django.setup()
# this line must be after django.setup() for logging configure
logger = logging.getLogger('vrobbler')
def main():
# to get configured settings
from django.conf import settings

View File

@ -280,35 +280,35 @@
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: '#007bff',
borderWidth: 4,
pointBackgroundColor: '#007bff'
}]
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
}
scales: {
yAxes: [{
ticks: {
beginAtZero: false
}
}]
},
legend: {
display: false
}
}
})
})()

View File

@ -3,7 +3,7 @@
{% block content %}
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
@ -18,82 +18,134 @@
</div>
</div>
<canvas class="my-4 w-100" id="myChart" width="900" height="380"></canvas>
<canvas class="my-4 w-100" id="myChart" width="900" height="220"></canvas>
<div class="container">
<h2>Top 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>
<th scope="col">Album</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>
<td>{{track.album.name}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<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>
</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="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>
</ul>
<h2>Latest 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 class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Track</th>
<th scope="col">Artist</th>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
{% for scrobble in object_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.track.title}}</td>
<td>{{scrobble.track.artist.name}}</td>
<td>{{scrobble.source}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show" 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>
<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>
<th scope="col">Season & Episode</th>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
{% for scrobble in video_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.video.title}}</td>
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}</td>
<td>{% if scrobble.video.tv_series %}{{scrobble.video.season_number}}, {{scrobble.video.episode_number}}{% endif %}</td>
<td>{{scrobble.source}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="tab-pane fade show active" 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>
</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">Latest Listened</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">Latest Watched</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">Track</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for scrobble in object_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.track.title}}</td>
<td>{{scrobble.track.artist.name}}</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>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
{% for scrobble in video_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td>{% if scrobble.video.tv_series %}E{{scrobble.video.season_number}}S{{scrobble.video.season_number}} -{% endif %} {{scrobble.video.title}}</td>
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}</td>
<td>{{scrobble.source}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
{% endblock %}