Compare commits

...

9 Commits
0.6.1 ... 0.6.2

Author SHA1 Message Date
907ef802bc Bump version to 0.6.2
Releases

* Fix for double scrobbling
* Fix error messages
* Fix bug in IMDB lookup when runtime is missing
* Fix filter order with annotations for top tracks and artists
* Fix error in completion percentage issues
2023-01-17 00:05:54 -05:00
d700b581a1 Fix filter order with annotations
We were getting all artists of all time, not just for the time period
2023-01-17 00:02:20 -05:00
7605c672f6 Fix str rep for scrobbles 2023-01-16 23:54:19 -05:00
8d1df806d7 Fix IMDB fetch when runtimes are not present 2023-01-16 23:46:04 -05:00
0f562b7c58 Fix resume bug, stop trying to avoid resuming
Turns out trying to not resume in-progress Scrobbles is super painful.
Maybe I'll come up with a better idea later, but for now, I'd rather
just resurrect old paused scrobbles of past tracks, rather than
completely mess up all other aspects of scrobbling.
2023-01-16 23:44:49 -05:00
fe53b68714 Fix silly error message 2023-01-16 20:40:02 -05:00
7e2915850f Fix double scrobbling for real 2023-01-16 20:39:27 -05:00
90687a6b43 Fix complicated completion percentage 2023-01-16 19:36:10 -05:00
07cfb03eb6 Fix really irritating double scrobble bug 2023-01-16 19:34:40 -05:00
11 changed files with 43 additions and 117 deletions

View File

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

View File

@ -71,8 +71,8 @@ def top_tracks(filter: str = "today", limit: int = 15) -> List["Track"]:
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR)
return (
Track.objects.annotate(num_scrobbles=Count("scrobble", distinct=True))
.filter(time_filter)
Track.objects.filter(time_filter)
.annotate(num_scrobbles=Count("scrobble", distinct=True))
.order_by("-num_scrobbles")[:limit]
)
@ -93,10 +93,8 @@ def top_artists(filter: str = "today", limit: int = 15) -> List["Artist"]:
)
return (
Artist.objects.annotate(
num_scrobbles=Count("track__scrobble", distinct=True)
)
.filter(time_filter)
Artist.objects.filter(time_filter)
.annotate(num_scrobbles=Count("track__scrobble", distinct=True))
.order_by("-num_scrobbles")[:limit]
)

View File

@ -94,7 +94,6 @@ class Album(TimeStampedModel):
class Track(ScrobblableMixin):
RESUME_LIMIT = getattr(settings, 'MUSIC_RESUME_LIMIT', 60 * 60)
COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)
class Opinion(models.IntegerChoices):

View File

@ -35,7 +35,6 @@ class Podcast(TimeStampedModel):
class Episode(ScrobblableMixin):
RESUME_LIMIT = getattr(settings, 'PODCAST_RESUME_LIMIT', 180 * 60)
COMPLETION_PERCENT = getattr(settings, 'PODCAST_COMPLETION_PERCENT', 90)
podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)

View File

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

View File

@ -19,7 +19,11 @@ def lookup_video_from_imdb(imdb_id: str) -> dict:
lookup_id = imdb_id.strip('tt')
media = imdb_client.get_movie(lookup_id)
run_time_seconds = int(media.get('runtimes')[0]) * 60
run_time_seconds = 60 * 60
runtimes = media.get("runtimes")
if runtimes:
run_time_seconds = int(runtimes)[0] * 60
# Ticks otherwise known as miliseconds
run_time_ticks = run_time_seconds * 1000 * 1000

View File

@ -70,61 +70,8 @@ class Scrobble(TimeStampedModel):
return media_obj
def __str__(self):
return f"Scrobble of {self.media_obj} {self.timestamp.year}-{self.timestamp.month}"
def resumable(self, playback_ticks):
"""Check if a scrobble is not finished or beyond the configured resume limit.
The idea here is to check whether a scrobble should be resumed, or a new
one created. If this method returns true, we should update an existing
scrobble, suggesting the user just paused their scrobble. This limit
should be different for different media. We are more likely to pause a video
or sports event for a while, and expect to resume it than an audio track or
a podcast.
"""
diff = None
# Default finish expectation
percent_for_completion = 100
# By default, assume we're not beyond resume limits
# This is to avoid spam scrobbles if webhooks go crazy
beyond_resume_limit = False
now = timezone.now()
if self.playback_position_ticks == playback_ticks:
# shortcircut in the case where we've resumed a track at the same playback ticks
return True
if self.video:
diff = timedelta(seconds=Video.RESUME_LIMIT)
percent_for_completion = Video.COMPLETION_PERCENT
if self.track:
diff = timedelta(seconds=Track.RESUME_LIMIT)
percent_for_completion = Track.COMPLETION_PERCENT
if self.podcast_episode:
diff = timedelta(seconds=Episode.RESUME_LIMIT)
percent_for_completion = Episode.COMPLETION_PERCENT
if self.sport_event:
diff = timedelta(seconds=SportEvent.RESUME_LIMIT)
percent_for_completion = SportEvent.COMPLETION_PERCENT
if diff and self.timestamp:
beyond_resume_limit = self.timestamp + diff <= now
finished = self.percent_played >= percent_for_completion
resumable = not finished or not beyond_resume_limit
if not finished:
logger.debug(
f"{self} resumable, percent played {self.percent_played} is less than {percent_for_completion}"
)
if not beyond_resume_limit:
logger.debug(
f"{self} resumable, started less than {diff.seconds} seconds ago"
)
return not finished and not beyond_resume_limit
timestamp = self.timestamp.strftime('%Y-%m-%d')
return f"Scrobble of {self.media_obj} ({timestamp})"
@classmethod
def create_or_update_for_video(
@ -133,13 +80,15 @@ class Scrobble(TimeStampedModel):
scrobble_data['video_id'] = video.id
scrobble = (
cls.objects.filter(video=video, user_id=user_id)
cls.objects.filter(
video=video,
user_id=user_id,
played_to_completion=False,
)
.order_by('-modified')
.first()
)
if scrobble and scrobble.resumable(
scrobble_data['playback_position_ticks']
):
if scrobble:
logger.info(
f"Found existing scrobble for video {video}, updating",
{"scrobble_data": scrobble_data},
@ -164,13 +113,15 @@ class Scrobble(TimeStampedModel):
scrobble_data['track_id'] = track.id
scrobble = (
cls.objects.filter(track=track, user_id=user_id)
cls.objects.filter(
track=track,
user_id=user_id,
played_to_completion=False,
)
.order_by('-modified')
.first()
)
if scrobble and scrobble.resumable(
scrobble_data['playback_position_ticks']
):
if scrobble:
logger.debug(
f"Found existing scrobble for track {track}, updating",
{"scrobble_data": scrobble_data},
@ -192,13 +143,15 @@ class Scrobble(TimeStampedModel):
scrobble_data['podcast_episode_id'] = episode.id
scrobble = (
cls.objects.filter(podcast_episode=episode, user_id=user_id)
cls.objects.filter(
podcast_episode=episode,
user_id=user_id,
played_to_completion=False,
)
.order_by('-modified')
.first()
)
if scrobble and scrobble.resumable(
scrobble_data['playback_position_ticks']
):
if scrobble:
logger.debug(
f"Found existing scrobble for podcast {episode}, updating",
{"scrobble_data": scrobble_data},
@ -219,13 +172,15 @@ class Scrobble(TimeStampedModel):
) -> "Scrobble":
scrobble_data['sport_event_id'] = event.id
scrobble = (
cls.objects.filter(sport_event=event, user_id=user_id)
cls.objects.filter(
sport_event=event,
user_id=user_id,
played_to_completion=False,
)
.order_by('-modified')
.first()
)
if scrobble and scrobble.resumable(
scrobble_data['playback_position_ticks']
):
if scrobble:
logger.debug(
f"Found existing scrobble for sport event {event}, updating",
{"scrobble_data": scrobble_data},
@ -246,8 +201,6 @@ class Scrobble(TimeStampedModel):
scrobble_status = scrobble_data.pop('mopidy_status', None)
if not scrobble_status:
scrobble_status = scrobble_data.pop('jellyfin_status', None)
if not scrobble_status:
scrobble_status = 'resumed'
logger.debug(f"Scrobbling to {scrobble} with status {scrobble_status}")
scrobble.update_ticks(scrobble_data)
@ -287,7 +240,7 @@ class Scrobble(TimeStampedModel):
check_scrobble_for_finish(self)
def pause(self) -> None:
if self.is_paused and not self.played_to_completion:
if self.is_paused:
logger.warning("Scrobble already paused")
return
self.is_paused = True
@ -295,15 +248,10 @@ class Scrobble(TimeStampedModel):
check_scrobble_for_finish(self)
def resume(self) -> None:
if self.is_paused or not self.played_to_completion:
if self.is_paused or not self.in_progress:
self.is_paused = False
self.in_progress = True
return self.save(
update_fields=[
"is_paused",
"in_progress",
]
)
return self.save(update_fields=["is_paused", "in_progress"])
def update_ticks(self, data) -> None:
self.playback_position_ticks = data.get("playback_position_ticks")

View File

@ -1,5 +1,4 @@
import logging
from typing import Any, Optional
from urllib.parse import unquote
from dateutil.parser import ParserError, parse
@ -68,17 +67,11 @@ def parse_mopidy_uri(uri: str) -> dict:
def check_scrobble_for_finish(scrobble: "Scrobble") -> None:
completion_percent = getattr(settings, "MUSIC_COMPLETION_PERCENT", 95)
if scrobble.video:
completion_percent = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
if scrobble.podcast_episode:
completion_percent = getattr(
settings, "PODCAST_COMPLETION_PERCENT", 25
)
completion_percent = scrobble.media_obj.COMPLETION_PERCENT
if scrobble.percent_played >= completion_percent:
logger.debug(
f"Beyond completion percent {completion_percent}, finishing scrobble"
)
logger.debug(f"Completion percent {completion_percent} met, finishing")
scrobble.in_progress = False
scrobble.is_paused = False
scrobble.played_to_completion = True

View File

@ -39,18 +39,6 @@ from vrobbler.apps.music.aggregators import (
logger = logging.getLogger(__name__)
TRUTHY_VALUES = [
'true',
'1',
't',
'y',
'yes',
'yeah',
'yup',
'certainly',
'uh-huh',
]
class RecentScrobbleList(ListView):
model = Scrobble

View File

@ -39,7 +39,6 @@ class Team(TimeStampedModel):
class SportEvent(ScrobblableMixin):
RESUME_LIMIT = getattr(settings, 'SPORT_RESUME_LIMIT', (12 * 60) * 60)
COMPLETION_PERCENT = getattr(settings, 'SPORT_COMPLETION_PERCENT', 90)
class Type(models.TextChoices):

View File

@ -33,7 +33,6 @@ class Series(TimeStampedModel):
class Video(ScrobblableMixin):
RESUME_LIMIT = getattr(settings, 'VIDEO_RESUME_LIMIT', (12 * 60) * 60)
COMPLETION_PERCENT = getattr(settings, 'VIDEO_COMPLETION_PERCENT', 90)
class VideoType(models.TextChoices):