Refactor scrobbling code and add Music

If you send Track data from the Jellyfin Webhook plugin, we'll do the
right thing with it. Lots more to do to clean this up, but it also
involved moduralizing the code for scrobbling so it's a little simpler
to understand what's going on.
This commit is contained in:
2023-01-07 19:34:11 -05:00
parent 9c0d85c5d2
commit 638be0b56a
15 changed files with 635 additions and 129 deletions

View File

View File

@ -0,0 +1,29 @@
from django.contrib import admin
from music.models import Artist, Album, Track
@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = ("name", "year", "musicbrainz_id")
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"
list_display = (
"title",
"album",
"artist",
"run_time",
"musicbrainz_id",
)
list_filter = ("album", "artist")
ordering = ("-created",)

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MusicConfig(AppConfig):
name = 'music'

View File

@ -0,0 +1,16 @@
JELLYFIN_POST_KEYS = {
'ITEM_TYPE': 'ItemType',
'RUN_TIME_TICKS': 'RunTimeTicks',
'RUN_TIME': 'RunTime',
'TITLE': 'Name',
'TIMESTAMP': 'UtcTimestamp',
'YEAR': 'Year',
'PLAYBACK_POSITION_TICKS': 'PlaybackPositionTicks',
'PLAYBACK_POSITION': 'PlaybackPosition',
'ARTIST_MB_ID': 'Provider_musicbrainzartist',
'ALBUM_MB_ID': 'Provider_musicbrainzalbum',
'RELEASEGROUP_MB_ID': 'Provider_musicbrainzreleasegroup',
'TRACK_MB_ID': 'Provider_musicbrainztrack',
'ALBUM_NAME': 'Album',
'ARTIST_NAME': 'Artist',
}

View File

@ -0,0 +1,156 @@
# Generated by Django 4.1.5 on 2023-01-07 19:37
from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Album',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'created',
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name='created'
),
),
(
'modified',
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name='modified'
),
),
('name', models.CharField(max_length=255)),
('year', models.IntegerField()),
(
'musicbrainz_id',
models.CharField(blank=True, max_length=255, null=True),
),
(
'musicbrainz_releasegroup_id',
models.CharField(blank=True, max_length=255, null=True),
),
(
'musicbrainz_albumartist_id',
models.CharField(blank=True, max_length=255, null=True),
),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
migrations.CreateModel(
name='Artist',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'created',
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name='created'
),
),
(
'modified',
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name='modified'
),
),
('name', models.CharField(max_length=255)),
(
'musicbrainz_id',
models.CharField(blank=True, max_length=255, null=True),
),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
migrations.CreateModel(
name='Track',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'created',
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name='created'
),
),
(
'modified',
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name='modified'
),
),
(
'title',
models.CharField(blank=True, max_length=255, null=True),
),
(
'musicbrainz_id',
models.CharField(blank=True, max_length=255, null=True),
),
(
'run_time',
models.CharField(blank=True, max_length=8, null=True),
),
(
'run_time_ticks',
models.PositiveBigIntegerField(blank=True, null=True),
),
(
'album',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to='music.album',
),
),
(
'artist',
models.ForeignKey(
on_delete=django.db.models.deletion.DO_NOTHING,
to='music.artist',
),
),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
]

View File

@ -0,0 +1,99 @@
import logging
from typing import Dict, Optional
from django.db import models
from django_extensions.db.models import TimeStampedModel
from django.utils.translation import gettext_lazy as _
from vrobbler.apps.music.constants import JELLYFIN_POST_KEYS as KEYS
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
class Album(TimeStampedModel):
name = models.CharField(max_length=255)
year = models.IntegerField()
musicbrainz_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)
def __str__(self):
return self.name
class Artist(TimeStampedModel):
name = models.CharField(max_length=255)
musicbrainz_id = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.name
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)
run_time = models.CharField(max_length=8, **BNULL)
run_time_ticks = models.PositiveBigIntegerField(**BNULL)
def __str__(self):
return f"{self.title} by {self.artist}"
@classmethod
def find_or_create(cls, data_dict: Dict) -> Optional["Track"]:
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
the video and, if need, TV Series, creating both if they don't yet
exist.
"""
artist = data_dict.get(KEYS["ARTIST_NAME"], None)
artist_musicbrainz_id = data_dict.get(KEYS["ARTIST_MB_ID"], None)
if not artist or not artist_musicbrainz_id:
logger.warning(
f"No artist or artist musicbrainz ID found in message from Jellyfin, not scrobbling"
)
return
artist, artist_created = Artist.objects.get_or_create(
name=artist, musicbrainz_id=artist_musicbrainz_id
)
if artist_created:
logger.debug(f"Created new album {artist}")
else:
logger.debug(f"Found album {artist}")
album = None
album_name = data_dict.get(KEYS["ALBUM_NAME"], None)
if album_name:
album_dict = {
"name": album_name,
"year": data_dict.get(KEYS["YEAR"], ""),
"musicbrainz_id": data_dict.get(KEYS['ALBUM_MB_ID']),
"musicbrainz_releasegroup_id": data_dict.get(
KEYS["RELEASEGROUP_MB_ID"]
),
"musicbrainz_albumartist_id": data_dict.get(
KEYS["ARTIST_MB_ID"]
),
}
album, album_created = Album.objects.get_or_create(**album_dict)
if album_created:
logger.debug(f"Created new album {album}")
else:
logger.debug(f"Found album {album}")
track_dict = {
"title": data_dict.get("Name", ""),
"musicbrainz_id": data_dict.get(KEYS["TRACK_MB_ID"], None),
"run_time_ticks": data_dict.get(KEYS["RUN_TIME_TICKS"], None),
"run_time": data_dict.get(KEYS["RUN_TIME"], None),
"album_id": getattr(album, "id", None),
"artist_id": artist.id,
}
track, created = cls.objects.get_or_create(**track_dict)
if created:
logger.debug(f"Created new track: {track}")
else:
logger.debug(f"Found track{track}")
return track

View File

@ -6,8 +6,9 @@ from scrobbles.models import Scrobble
class ScrobbleAdmin(admin.ModelAdmin):
date_hierarchy = "timestamp"
list_display = (
"video",
"timestamp",
"video",
"track",
"source",
"playback_position",
"in_progress",

View File

@ -0,0 +1,3 @@
#!/usr/bin/env python3
JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"]
JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]

View File

@ -0,0 +1,36 @@
# Generated by Django 4.1.5 on 2023-01-07 20:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('music', '0001_initial'),
('videos', '0003_alter_video_run_time_ticks'),
('scrobbles', '0005_alter_scrobble_playback_position_ticks'),
]
operations = [
migrations.AddField(
model_name='scrobble',
name='track',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to='music.track',
),
),
migrations.AlterField(
model_name='scrobble',
name='video',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to='videos.video',
),
),
]

View File

@ -1,15 +1,27 @@
import logging
from datetime import timedelta
from typing import Optional
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.utils import timezone
from django_extensions.db.models import TimeStampedModel
from music.models import Track
from videos.models import Video
logger = logging.getLogger('__name__')
User = get_user_model()
BNULL = {"blank": True, "null": True}
VIDEO_BACKOFF = getattr(settings, 'VIDEO_BACKOFF_MINUTES')
TRACK_BACKOFF = getattr(settings, 'MUSIC_BACKOFF_SECONDS')
VIDEO_WAIT_PERIOD = getattr(settings, 'VIDEO_WAIT_PERIOD_DAYS')
TRACK_WAIT_PERIOD = getattr(settings, 'MUSIC_WAIT_PERIOD_MINUTES')
class Scrobble(TimeStampedModel):
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING)
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
user = models.ForeignKey(
User, blank=True, null=True, on_delete=models.DO_NOTHING
)
@ -26,8 +38,138 @@ class Scrobble(TimeStampedModel):
@property
def percent_played(self) -> int:
return int(
(self.playback_position_ticks / self.video.run_time_ticks) * 100
(self.playback_position_ticks / self.media_run_time_ticks) * 100
)
@property
def media_run_time_ticks(self) -> int:
if self.video:
return self.video.run_time_ticks
if self.track:
return self.track.run_time_ticks
# this is hacky, but want to avoid divide by zero
return 1
def is_stale(self, backoff, wait_period) -> bool:
scrobble_is_stale = self.in_progress and self.modified > wait_period
# Check if found in progress scrobble is more than a day old
if scrobble_is_stale:
logger.info(
'Found a in-progress scrobble for this item more than a day old, creating a new scrobble'
)
delete_stale_scrobbles = getattr(
settings, "DELETE_STALE_SCROBBLES", True
)
if delete_stale_scrobbles:
logger.info(
'Deleting {scrobble} that has been in-progress too long'
)
self.delete()
return scrobble_is_stale
def __str__(self):
return f"Scrobble of {self.video} {self.timestamp.year}-{self.timestamp.month}"
media = None
if self.video:
media = self.video
if self.track:
media = self.track
return (
f"Scrobble of {media} {self.timestamp.year}-{self.timestamp.month}"
)
@classmethod
def create_or_update_for_video(
cls, video: "Video", user_id: int, jellyfin_data: dict
) -> "Scrobble":
jellyfin_data['video_id'] = video.id
logger.debug(
f"Creating or updating scrobble for video {video} with data {jellyfin_data}"
)
scrobble = (
Scrobble.objects.filter(video=video, user_id=user_id)
.order_by('-modified')
.first()
)
# Backoff is how long until we consider this a new scrobble
backoff = timezone.now() + timedelta(minutes=VIDEO_BACKOFF)
wait_period = timezone.now() + timedelta(days=VIDEO_WAIT_PERIOD)
return cls.update_or_create(
scrobble, backoff, wait_period, jellyfin_data
)
@classmethod
def create_or_update_for_track(
cls, track: "Track", user_id: int, jellyfin_data: dict
) -> "Scrobble":
jellyfin_data['track_id'] = track.id
logger.debug(
f"Creating or updating scrobble for track {track}",
{"jellyfin_data": jellyfin_data},
)
scrobble = (
Scrobble.objects.filter(track=track, user_id=user_id)
.order_by('-modified')
.first()
)
backoff = timezone.now() + timedelta(minutes=TRACK_BACKOFF)
wait_period = timezone.now() + timedelta(minutes=TRACK_WAIT_PERIOD)
return cls.update_or_create(
scrobble, backoff, wait_period, jellyfin_data
)
@classmethod
def update_or_create(
cls,
scrobble: Optional["Scrobble"],
backoff,
wait_period,
jellyfin_data: dict,
) -> Optional["Scrobble"]:
scrobble_is_stale = False
if scrobble:
scrobble_is_finished = (
not scrobble.in_progress and scrobble.modified < backoff
)
if scrobble_is_finished:
logger.info(
'Found a very recent scrobble for this item, holding off scrobbling again'
)
return
scrobble_is_stale = scrobble.is_stale(backoff, wait_period)
if not scrobble or scrobble_is_stale:
# If we default this to "" we can probably remove this
jellyfin_data['scrobble_log'] = ""
scrobble = cls.objects.create(
**jellyfin_data,
)
else:
for key, value in jellyfin_data.items():
setattr(scrobble, key, value)
scrobble.save()
# If we hit our completion threshold, save it and get ready
# to scrobble again if we re-watch this.
if scrobble.percent_played >= getattr(
settings, "PERCENT_FOR_COMPLETION", 95
):
scrobble.in_progress = False
scrobble.playback_position_ticks = scrobble.media_run_time_ticks
scrobble.save()
if scrobble.percent_played % 5 == 0:
if getattr(settings, "KEEP_DETAILED_SCROBBLE_LOGS", False):
scrobble.scrobble_log += f"\n{str(scrobble.timestamp)} - {scrobble.playback_position} - {str(scrobble.playback_position_ticks)} - {str(scrobble.percent_played)}%"
scrobble.save(update_fields=['scrobble_log'])
return scrobble

View File

@ -5,17 +5,20 @@ from datetime import datetime, timedelta
from dateutil.parser import parse
from django.conf import settings
from django.db.models.fields import timezone
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.list import ListView
from music.models import Track
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from scrobbles.constants import (
JELLYFIN_AUDIO_ITEM_TYPES,
JELLYFIN_VIDEO_ITEM_TYPES,
)
from scrobbles.models import Scrobble
from scrobbles.serializers import ScrobbleSerializer
from videos.models import Series, Video
from vrobbler.settings import DELETE_STALE_SCROBBLES
from django.utils import timezone
from videos.models import Video
logger = logging.getLogger(__name__)
@ -38,11 +41,11 @@ class RecentScrobbleList(ListView):
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
now = timezone.now()
last_ten_minutes = timezone.now() - timedelta(minutes=10)
last_three_minutes = timezone.now() - timedelta(minutes=3)
# Find scrobbles from the last 10 minutes
data['now_playing_list'] = Scrobble.objects.filter(
in_progress=True,
timestamp__gte=last_ten_minutes,
timestamp__gte=last_three_minutes,
timestamp__lte=now,
)
return data
@ -66,122 +69,57 @@ def scrobble_endpoint(request):
@api_view(['POST'])
def jellyfin_websocket(request):
data_dict = request.data
media_type = data_dict["ItemType"]
imdb_id = data_dict.get("Provider_imdb", None)
if not imdb_id:
logger.error(
"No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
)
return Response({}, status=status.HTTP_400_BAD_REQUEST)
# Check if it's a TV Episode
video_dict = {
"title": data_dict.get("Name", ""),
"imdb_id": imdb_id,
"video_type": Video.VideoType.MOVIE,
"year": data_dict.get("Year", ""),
}
if media_type == 'Episode':
series_name = data_dict["SeriesName"]
series, series_created = Series.objects.get_or_create(name=series_name)
video_dict['video_type'] = Video.VideoType.TV_EPISODE
video_dict["tv_series_id"] = series.id
video_dict["episode_number"] = data_dict.get("EpisodeNumber", "")
video_dict["season_number"] = data_dict.get("SeasonNumber", "")
video_dict["tvdb_id"] = data_dict.get("Provider_tvdb", None)
video_dict["tvrage_id"] = data_dict.get("Provider_tvrage", None)
# For making things easier to build new input processors
if getattr(settings, "DUMP_REQUEST_DATA", False):
json_data = json.dumps(data_dict, indent=4)
logger.debug(f"{json_data}")
video, video_created = Video.objects.get_or_create(**video_dict)
media_type = data_dict.get("ItemType", "")
if video_created:
video.overview = data_dict["Overview"]
video.tagline = data_dict["Tagline"]
video.run_time_ticks = data_dict["RunTimeTicks"]
video.run_time = data_dict["RunTime"]
video.save()
track = None
video = None
existing_scrobble = False
if media_type in JELLYFIN_AUDIO_ITEM_TYPES:
if not data_dict.get("Provider_musicbrainztrack", None):
logger.error(
"No MBrainz Track ID received. This is likely because all metadata is bad, not scrobbling"
)
return Response({}, status=status.HTTP_400_BAD_REQUEST)
track = Track.find_or_create(data_dict)
if media_type in JELLYFIN_VIDEO_ITEM_TYPES:
if not data_dict.get("Provider_imdb", None):
logger.error(
"No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
)
return Response({}, status=status.HTTP_400_BAD_REQUEST)
video = Video.find_or_create(data_dict)
# Now we run off a scrobble
timestamp = parse(data_dict["UtcTimestamp"])
scrobble_dict = {
'video_id': video.id,
'user_id': request.user.id,
'in_progress': True,
jellyfin_data = {
"user_id": request.user.id,
"timestamp": parse(data_dict.get("UtcTimestamp")),
"playback_position_ticks": data_dict.get("PlaybackPositionTicks"),
"playback_position": data_dict.get("PlaybackPosition"),
"source": data_dict.get('ClientName'),
"source_id": data_dict.get('MediaSourceId'),
"is_paused": data_dict.get("IsPaused") in TRUTHY_VALUES,
}
existing_scrobble = (
Scrobble.objects.filter(video=video, user_id=request.user.id)
.order_by('-modified')
.first()
)
minutes_from_now = timezone.now() + timedelta(minutes=15)
a_day_from_now = timezone.now() + timedelta(days=1)
existing_finished_scrobble = (
existing_scrobble
and not existing_scrobble.in_progress
and existing_scrobble.modified < minutes_from_now
)
existing_in_progress_scrobble = (
existing_scrobble
and existing_scrobble.in_progress
and existing_scrobble.modified > a_day_from_now
)
delete_stale_scrobbles = getattr(settings, "DELETE_STALE_SCROBBLES", True)
if existing_finished_scrobble:
logger.info(
'Found a scrobble for this video less than 15 minutes ago, holding off scrobbling again'
scrobble = None
if video:
scrobble = Scrobble.create_or_update_for_video(
video, request.user.id, jellyfin_data
)
return Response(video_dict, status=status.HTTP_204_NO_CONTENT)
# Check if found in progress scrobble is more than a day old
if existing_in_progress_scrobble:
logger.info(
'Found a scrobble for this video more than a day old, creating a new scrobble'
)
scrobble = existing_in_progress_scrobble
scrobble_created = False
else:
if existing_in_progress_scrobble and delete_stale_scrobbles:
existing_in_progress_scrobble.delete()
scrobble, scrobble_created = Scrobble.objects.get_or_create(
**scrobble_dict
if track:
scrobble = Scrobble.create_or_update_for_track(
track, request.user.id, jellyfin_data
)
if scrobble_created:
# If we newly created this, capture the client we're watching from
scrobble.source = data_dict['ClientName']
scrobble.source_id = data_dict['MediaSourceId']
scrobble.scrobble_log = ""
if not scrobble:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
# Update a found scrobble with new position and timestamp
scrobble.playback_position_ticks = data_dict["PlaybackPositionTicks"]
scrobble.playback_position = data_dict["PlaybackPosition"]
scrobble.timestamp = parse(data_dict["UtcTimestamp"])
scrobble.is_paused = data_dict["IsPaused"] in TRUTHY_VALUES
scrobble.save(
update_fields=[
'playback_position_ticks',
'playback_position',
'timestamp',
'is_paused',
]
return Response(
{'scrobble_id': scrobble.id}, status=status.HTTP_201_CREATED
)
# If we hit our completion threshold, save it and get ready
# to scrobble again if we re-watch this.
if scrobble.percent_played >= getattr(
settings, "PERCENT_FOR_COMPLETION", 95
):
scrobble.in_progress = False
scrobble.playback_position_ticks = video.run_time_ticks
scrobble.save()
if scrobble.percent_played % 5 == 0:
if getattr(settings, "KEEP_DETAILED_SCROBBLE_LOGS", False):
scrobble.scrobble_log += f"\n{str(scrobble.timestamp)} - {scrobble.playback_position} - {str(scrobble.playback_position_ticks)} - {str(scrobble.percent_played)}%"
scrobble.save(update_fields=['scrobble_log'])
logger.debug(f"You are {scrobble.percent_played}% through {video}")
return Response(video_dict, status=status.HTTP_201_CREATED)

View File

@ -1,7 +1,10 @@
import logging
from typing import Dict, Tuple
from django.db import models
from django_extensions.db.models import TimeStampedModel
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
@ -50,3 +53,45 @@ class Video(TimeStampedModel):
if self.video_type == self.VideoType.TV_EPISODE:
return f"{self.tv_series} - Season {self.season_number}, Episode {self.episode_number}"
return self.title
@classmethod
def find_or_create(cls, data_dict: Dict) -> "Video":
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
the video and, if need, TV Series, creating both if they don't yet
exist.
"""
video_dict = {
"title": data_dict.get("Name", ""),
"imdb_id": data_dict.get("Provider_imdb", None),
"video_type": Video.VideoType.MOVIE,
"year": data_dict.get("Year", ""),
"overview": data_dict.get("Overview", None),
"tagline": data_dict.get("Tagline", None),
"run_time_ticks": data_dict.get("RunTimeTicks", None),
"run_time": data_dict.get("RunTime", None),
}
if data_dict.get("ItemType", "") == "Episode":
series_name = data_dict.get("SeriesName", "")
series, series_created = Series.objects.get_or_create(name=series_name)
if series_created:
logger.debug(f"Created new series {series}")
else:
logger.debug(f"Found series {series}")
video_dict['video_type'] = Video.VideoType.TV_EPISODE
video_dict["tv_series_id"] = series.id
video_dict["tvdb_id"] = data_dict.get("Provider_tvdb", None)
video_dict["tvrage_id"] = data_dict.get("Provider_tvrage", None)
video_dict["episode_number"] = data_dict.get("EpisodeNumber", "")
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:
logger.debug(f"Found video {video}")
return video

View File

@ -37,8 +37,22 @@ KEEP_DETAILED_SCROBBLE_LOGS = os.getenv(
"VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False
)
# Should we cull old in-progress scrobbles that are beyond the wait period for resuming?
DELETE_STALE_SCROBBLES = os.getenv("VROBBLER_DELETE_STALE_SCROBBLES", True)
# Used to dump data coming from srobbling sources, helpful for building new inputs
DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", False)
VIDEO_BACKOFF_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 15)
MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 5)
# If you stop waching or listening to a track, how long should we wait before we
# give up on the old scrobble and start a new one? This could also be considered
# a "continue in progress scrobble" time period. So if you pause the media and
# start again, should it be a new scrobble.
VIDEO_WAIT_PERIOD_DAYS = os.getenv("VROBBLER_VIDEO_WAIT_PERIOD_DAYS", 1)
MUSIC_WAIT_PERIOD_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 1)
TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@ -70,6 +84,7 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
"scrobbles",
"videos",
"music",
"rest_framework",
"allauth",
"allauth.account",

View File

@ -6,23 +6,44 @@
{% if now_playing_list %}
<h2>Now playing</h2>
{% for scrobble in now_playing_list %}
<dl class="latest-scrobble">
<dt>{{scrobble.video.title}} - {{scrobble.video}}</dt>
<dd>
{{scrobble.timestamp|date:"D, M j Y"}} |
<a href="https://www.imdb.com/title/{{scrobble.video.imdb_id}}">IMDB</a>
<div class="progress-bar">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
</dd>
</dl>
{% if scrobble.video %}
<dl class="latest-scrobble">
<dt>{{scrobble.video.title}} - {{scrobble.video}}</dt>
<dd>
{{scrobble.timestamp|date:"D, M j Y"}} |
<a href="https://www.imdb.com/title/{{scrobble.video.imdb_id}}">IMDB</a>
<div class="progress-bar">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
</dd>
</dl>
{% endif %}
{% if scrobble.track %}
<dl class="latest-scrobble">
<dt>{{scrobble.track.title}} by {{scrobble.track.artist}} from {{scrobble.track.album}}</dt>
<dd>
{{scrobble.timestamp|date:"D, M j Y"}} |
<a href="https://www.imdb.com/title/{{scrobble.track.musicbrainz_id}}">MusicBrainz</a>
<div class="progress-bar">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
</dd>
</dl>
{% endif %}
<br />
{% endfor %}
{% endif %}
<h2>Last scrobbles</h2>
<ul>
{% for scrobble in object_list %}
<li>{{scrobble.timestamp|date:"D, M j Y"}}: <a href="https://www.imdb.com/title/{{scrobble.video.imdb_id}}">{{scrobble.video}}{% if scrobble.video.video_type == 'E' %} - {{scrobble.video.title}}{% endif %}</a></li>
<li>
{{scrobble.timestamp|date:"D, M j Y"}}:
{% if scrobble.video %}
📼 <a href="https://www.imdb.com/title/{{scrobble.video.imdb_id}}">{{scrobble.video}}{% if scrobble.video.video_type == 'E' %} - {{scrobble.video.title}}{% endif %}</a></li>
{% endif %}
{% if scrobble.track %}
🎶 <a href="https://musicbrainz.org/recording/{{scrobble.track.album.musicbrainz_id}}">{{scrobble.track}} by {{scrobble.track.artist}}</a></li>
{% endif %}
{% endfor %}
</ul>
{% endblock %}