Compare commits

..

2 Commits
0.4.0 ... 0.4.1

Author SHA1 Message Date
a09c6d6b92 Bump version to 0.4.1 2023-01-12 21:34:17 -05:00
3f8b29f5ee Dramatically simplify the scrobblig code 2023-01-12 21:33:45 -05:00
14 changed files with 274 additions and 200 deletions

View File

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

View File

@ -18,7 +18,7 @@ STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
def scrobble_counts(): def scrobble_counts():
finished_scrobbles_qs = Scrobble.objects.filter(in_progress=False) finished_scrobbles_qs = Scrobble.objects.filter(played_to_completion=True)
data = {} data = {}
data['today'] = finished_scrobbles_qs.filter( data['today'] = finished_scrobbles_qs.filter(
timestamp__gte=START_OF_TODAY timestamp__gte=START_OF_TODAY
@ -53,7 +53,7 @@ def week_of_scrobbles(media: str = 'tracks') -> dict[str, int]:
.filter( .filter(
timestamp__gte=start, timestamp__gte=start,
timestamp__lte=end, timestamp__lte=end,
in_progress=False, played_to_completion=True,
) )
.count() .count()
) )

View File

@ -0,0 +1,17 @@
# Generated by Django 4.1.5 on 2023-01-13 01:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('music', '0007_alter_album_artists'),
]
operations = [
migrations.AlterModelOptions(
name='track',
options={},
),
]

View File

@ -48,35 +48,38 @@ class Album(TimeStampedModel):
return self.artists.first() return self.artists.first()
def fix_metadata(self): def fix_metadata(self):
musicbrainzngs.set_useragent('vrobbler', '0.3.0') if not self.musicbrainz_albumartist_id or not self.year:
mb_data = musicbrainzngs.get_release_by_id( musicbrainzngs.set_useragent('vrobbler', '0.3.0')
self.musicbrainz_id, includes=['artists'] mb_data = musicbrainzngs.get_release_by_id(
) self.musicbrainz_id, includes=['artists']
if not self.musicbrainz_albumartist_id: )
self.musicbrainz_albumartist_id = mb_data['release'][ if not self.musicbrainz_albumartist_id:
'artist-credit' self.musicbrainz_albumartist_id = mb_data['release'][
][0]['artist']['id'] 'artist-credit'
if not self.year: ][0]['artist']['id']
self.year = mb_data['release']['date'][0:4] if not self.year:
self.save(update_fields=['musicbrainz_albumartist_id', 'year']) self.year = mb_data['release']['date'][0:4]
self.save(update_fields=['musicbrainz_albumartist_id', 'year'])
new_artist = Artist.objects.filter( new_artist = Artist.objects.filter(
musicbrainz_id=self.musicbrainz_albumartist_id musicbrainz_id=self.musicbrainz_albumartist_id
).first() ).first()
if self.musicbrainz_albumartist_id and new_artist: if self.musicbrainz_albumartist_id and new_artist:
self.artists.add(new_artist) self.artists.add(new_artist)
if not new_artist: if not new_artist:
for t in self.track_set.all(): for t in self.track_set.all():
self.artists.add(t.artist) self.artists.add(t.artist)
def fetch_artwork(self): def fetch_artwork(self):
try: if not self.cover_image:
img_data = musicbrainzngs.get_image_front(self.musicbrainz_id) try:
name = f"{self.name}_{self.uuid}.jpg" img_data = musicbrainzngs.get_image_front(self.musicbrainz_id)
self.cover_image = ContentFile(img_data, name=name) name = f"{self.name}_{self.uuid}.jpg"
self.cover_image = ContentFile(img_data, name=name)
except musicbrainzngs.ResponseError:
logger.warning(f'No cover art found for {self.name}')
self.cover_image = 'default-image-replace-me'
self.save() self.save()
except musicbrainzngs.ResponseError:
logger.warning(f'No cover art found for {self.name}')
@property @property
def mb_link(self): def mb_link(self):
@ -132,8 +135,10 @@ class Track(ScrobblableMixin):
logger.debug(f"Created new album {album}") logger.debug(f"Created new album {album}")
else: else:
logger.debug(f"Found album {album}") logger.debug(f"Found album {album}")
album.fix_metadata() album.fix_metadata()
album.fetch_artwork() if not album.cover_image:
album.fetch_artwork()
track_dict['album_id'] = getattr(album, "id", None) track_dict['album_id'] = getattr(album, "id", None)
track_dict['artist_id'] = artist.id track_dict['artist_id'] = artist.id

View File

@ -0,0 +1,22 @@
# Generated by Django 4.1.5 on 2023-01-13 01:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('podcasts', '0004_episode_number'),
]
operations = [
migrations.AlterModelOptions(
name='episode',
options={},
),
migrations.AlterField(
model_name='episode',
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -10,10 +10,11 @@ class ScrobbleAdmin(admin.ModelAdmin):
"timestamp", "timestamp",
"media_name", "media_name",
"media_type", "media_type",
"playback_percent",
"source", "source",
"playback_position",
"in_progress", "in_progress",
"is_paused", "is_paused",
"played_to_completion",
) )
list_filter = ("is_paused", "in_progress", "source", "track__artist") list_filter = ("is_paused", "in_progress", "source", "track__artist")
ordering = ("-timestamp",) ordering = ("-timestamp",)
@ -33,3 +34,6 @@ class ScrobbleAdmin(admin.ModelAdmin):
return "Track" return "Track"
if obj.podcast_episode: if obj.podcast_episode:
return "Podcast" return "Podcast"
def playback_percent(self, obj):
return obj.percent_played

View File

@ -45,7 +45,7 @@ class Scrobble(TimeStampedModel):
if ( if (
self.playback_position_ticks self.playback_position_ticks
and self.media_obj.run_time_ticks and self.media_obj.run_time_ticks
and source != 'Mopidy' and self.source != 'Mopidy'
): ):
return int( return int(
(self.playback_position_ticks / self.media_obj.run_time_ticks) (self.playback_position_ticks / self.media_obj.run_time_ticks)
@ -61,26 +61,6 @@ class Scrobble(TimeStampedModel):
return 0 return 0
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
@property @property
def media_obj(self): def media_obj(self):
media_obj = None media_obj = None
@ -171,60 +151,31 @@ class Scrobble(TimeStampedModel):
) -> Optional["Scrobble"]: ) -> Optional["Scrobble"]:
# Status is a field we get from Mopidy, which refuses to poll us # Status is a field we get from Mopidy, which refuses to poll us
mopidy_status = scrobble_data.pop('status', None) scrobble_status = scrobble_data.pop('mopidy_status', None)
scrobble_is_stale = False if not scrobble_status:
scrobble_status = scrobble_data.pop('jellyfin_status', None)
if not scrobble_status:
logger.warning(
f"No status update found in message, not scrobbling"
)
return
logger.debug(f"Scrobbling to {scrobble} with status {scrobble_status}")
if scrobble: if scrobble:
logger.debug(f"Updating scrobble ticks") scrobble.update_ticks(scrobble_data)
scrobble.playback_position_ticks = scrobble_data.get(
"playback_position_ticks"
)
scrobble.save(update_fields=['playback_position_ticks'])
if mopidy_status == "stopped": # On stop, stop progress and send it to the check for completion
logger.info(f"Mopidy sent a message to stop {scrobble}") if scrobble_status == "stopped":
if not scrobble: return scrobble.stop()
logger.warning(
'Mopidy sent us a stopped message, without ever starting'
)
return
# Mopidy finished a play, scrobble away # On pause, set is_paused and stop scrobbling
scrobble.in_progress = False if scrobble_status == "paused":
scrobble.played_to_completion = True return scrobble.pause()
scrobble.save(
update_fields=['in_progress', 'played_to_completion']
)
return scrobble
if mopidy_status == "paused": if scrobble_status == "resumed":
logger.info(f"Mopidy sent a message to pause {scrobble}") return scrobble.resume()
if not scrobble:
logger.info("Message to pause while not started, ignoring")
return
if scrobble.is_paused:
logger.info("Message to pause while paused, ignoring")
return
# Mopidy finished a play, scrobble away # We're not changing the scrobble, but we don't want to walk over an existing one
scrobble.is_paused = True
scrobble.save(update_fields=["is_paused"])
scrobble = check_scrobble_for_finish(scrobble)
return scrobble
if mopidy_status == "resumed":
logger.info(f"Mopidy sent a message to resume {scrobble}")
if not scrobble:
logger.info("Message to resume while not started, ignoring")
return
if not scrobble.is_paused:
logger.info("Message to resume while not paused, resuming")
# Mopidy finished a play, scrobble away
scrobble.is_paused = False
scrobble.save(update_fields=["is_paused"])
return scrobble
if scrobble and not mopidy_status:
scrobble_is_finished = ( scrobble_is_finished = (
not scrobble.in_progress and scrobble.modified < backoff not scrobble.in_progress and scrobble.modified < backoff
) )
@ -234,9 +185,7 @@ class Scrobble(TimeStampedModel):
) )
return return
scrobble_is_stale = scrobble.is_stale(backoff, wait_period) if not scrobble:
if (not scrobble or scrobble_is_stale) or mopidy_status:
# If we default this to "" we can probably remove this # If we default this to "" we can probably remove this
scrobble_data['scrobble_log'] = "" scrobble_data['scrobble_log'] = ""
scrobble = cls.objects.create( scrobble = cls.objects.create(
@ -252,3 +201,37 @@ class Scrobble(TimeStampedModel):
scrobble = check_scrobble_for_finish(scrobble) scrobble = check_scrobble_for_finish(scrobble)
return scrobble return scrobble
def stop(self):
if not self.in_progress:
logger.warning("Scrobble already stopped")
return
self.in_progress = False
self.save(update_fields=['in_progress'])
return check_scrobble_for_finish(self)
def pause(self):
if self.is_paused:
logger.warning("Scrobble already paused")
return
self.is_paused = True
self.save(update_fields=["is_paused"])
return check_scrobble_for_finish(self)
def resume(self):
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
def update_ticks(self, data):
self.playback_position_ticks = data.get("playback_position_ticks")
self.playback_position = data.get("playback_position")
logger.debug(
f"Updating scrobble ticks to {self.playback_position_ticks}"
)
self.save(
update_fields=['playback_position_ticks', 'playback_position']
)
return self

View File

@ -1,11 +1,14 @@
import logging import logging
from typing import Optional from typing import Optional
from dateutil.parser import parse
from django.utils import timezone from django.utils import timezone
from music.constants import JELLYFIN_POST_KEYS
from music.models import Track from music.models import Track
from podcasts.models import Episode from podcasts.models import Episode
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.utils import parse_mopidy_uri from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
from videos.models import Video
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,7 +44,7 @@ def mopidy_scrobble_podcast(
"timestamp": timezone.now(), "timestamp": timezone.now(),
"playback_position_ticks": data_dict.get("playback_time_ticks"), "playback_position_ticks": data_dict.get("playback_time_ticks"),
"source": "Mopidy", "source": "Mopidy",
"status": data_dict.get("status"), "mopidy_status": data_dict.get("status"),
} }
scrobble = None scrobble = None
@ -79,10 +82,11 @@ def mopidy_scrobble_track(
"timestamp": timezone.now(), "timestamp": timezone.now(),
"playback_position_ticks": data_dict.get("playback_time_ticks"), "playback_position_ticks": data_dict.get("playback_time_ticks"),
"source": "Mopidy", "source": "Mopidy",
"status": data_dict.get("status"), "mopidy_status": data_dict.get("status"),
} }
scrobble = None scrobble = None
if track: if track:
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering # Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
track.musicbrainz_id = data_dict.get("musicbrainz_track_id") track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
@ -91,3 +95,84 @@ def mopidy_scrobble_track(
track, user_id, mopidy_data track, user_id, mopidy_data
) )
return scrobble return scrobble
def create_jellyfin_scrobble_dict(data_dict: dict, user_id: int) -> dict:
jellyfin_status = "resumed"
if data_dict.get("IsPaused"):
jellyfin_status = "paused"
if data_dict.get("PlayedToCompletion"):
jellyfin_status = "stopped"
return {
"user_id": user_id,
"timestamp": parse(data_dict.get("UtcTimestamp")),
"playback_position_ticks": data_dict.get("PlaybackPositionTicks")
// 10000,
"playback_position": convert_to_seconds(
data_dict.get("PlaybackPosition")
),
"source": "Jellyfin",
"source_id": data_dict.get('MediaSourceId'),
"jellyfin_status": jellyfin_status,
}
def jellyfin_scrobble_track(
data_dict: dict, user_id: Optional[int]
) -> Optional[Scrobble]:
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
artist_dict = {
'name': data_dict.get(JELLYFIN_POST_KEYS["ARTIST_NAME"], None),
'musicbrainz_id': data_dict.get(
JELLYFIN_POST_KEYS["ARTIST_MB_ID"], None
),
}
album_dict = {
"name": data_dict.get(JELLYFIN_POST_KEYS["ALBUM_NAME"], None),
"musicbrainz_id": data_dict.get(JELLYFIN_POST_KEYS['ALBUM_MB_ID']),
}
# Convert ticks from Jellyfin from microseconds to nanoseconds
# Ain't nobody got time for nanoseconds
track_dict = {
"title": data_dict.get("Name", ""),
"run_time_ticks": data_dict.get(
JELLYFIN_POST_KEYS["RUN_TIME_TICKS"], None
)
// 10000,
"run_time": convert_to_seconds(
data_dict.get(JELLYFIN_POST_KEYS["RUN_TIME"], None)
),
}
track = Track.find_or_create(artist_dict, album_dict, track_dict)
# Prefer Mopidy MD IDs to Jellyfin, so skip if we already have one
if not track.musicbrainz_id:
track.musicbrainz_id = data_dict.get(
JELLYFIN_POST_KEYS["TRACK_MB_ID"], None
)
track.save()
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
return Scrobble.create_or_update_for_track(track, user_id, scrobble_dict)
def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
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
video = Video.find_or_create(data_dict)
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import Any from typing import Any, Optional
from urllib.parse import unquote from urllib.parse import unquote
from dateutil.parser import ParserError, parse from dateutil.parser import ParserError, parse
@ -61,26 +61,19 @@ def parse_mopidy_uri(uri: str) -> dict:
def check_scrobble_for_finish(scrobble: "Scrobble") -> None: def check_scrobble_for_finish(scrobble: "Scrobble") -> None:
completion_percent = getattr(settings, "MUSIC_COMPLETION_PERCENT", 90) completion_percent = getattr(settings, "MUSIC_COMPLETION_PERCENT", 95)
if scrobble.video:
completion_percent = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
if scrobble.podcast_episode: if scrobble.podcast_episode:
completion_percent = getattr( completion_percent = getattr(
settings, "PODCAST_COMPLETION_PERCENT", 25 settings, "PODCAST_COMPLETION_PERCENT", 25
) )
logger.debug(f"Completion set to {completion_percent}")
if scrobble.percent_played >= completion_percent: if scrobble.percent_played >= completion_percent:
logger.debug(
f"{scrobble} meets completion goal of {completion_percent}, finishing"
)
scrobble.in_progress = False scrobble.in_progress = False
scrobble.is_paused = False scrobble.is_paused = False
scrobble.playback_position_ticks = scrobble.media_obj.run_time_ticks scrobble.played_to_completion = True
scrobble.save( scrobble.save(
update_fields=[ update_fields=["in_progress", "is_paused", "played_to_completion"]
"in_progress",
"is_paused",
"playback_position_ticks",
]
) )
if scrobble.percent_played % 5 == 0: if scrobble.percent_played % 5 == 0:

View File

@ -18,16 +18,22 @@ from scrobbles.constants import (
JELLYFIN_VIDEO_ITEM_TYPES, JELLYFIN_VIDEO_ITEM_TYPES,
) )
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.scrobblers import (
jellyfin_scrobble_track,
jellyfin_scrobble_video,
mopidy_scrobble_podcast,
mopidy_scrobble_track,
)
from scrobbles.serializers import ScrobbleSerializer from scrobbles.serializers import ScrobbleSerializer
from scrobbles.utils import convert_to_seconds from scrobbles.utils import convert_to_seconds
from videos.models import Video from videos.models import Video
from vrobbler.apps.music.aggregators import ( from vrobbler.apps.music.aggregators import (
scrobble_counts, scrobble_counts,
top_artists, top_artists,
top_tracks, top_tracks,
week_of_scrobbles, week_of_scrobbles,
) )
from scrobbles.scrobblers import mopidy_scrobble_podcast, mopidy_scrobble_track
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,7 +61,6 @@ class RecentScrobbleList(ListView):
data['now_playing_list'] = Scrobble.objects.filter( data['now_playing_list'] = Scrobble.objects.filter(
in_progress=True, in_progress=True,
is_paused=False, is_paused=False,
timestamp__gte=last_eight_minutes,
timestamp__lte=now, timestamp__lte=now,
) )
data['video_scrobble_list'] = Scrobble.objects.filter( data['video_scrobble_list'] = Scrobble.objects.filter(
@ -98,79 +103,14 @@ def jellyfin_websocket(request):
json_data = json.dumps(data_dict, indent=4) json_data = json.dumps(data_dict, indent=4)
logger.debug(f"{json_data}") logger.debug(f"{json_data}")
scrobble = None
media_type = data_dict.get("ItemType", "") media_type = data_dict.get("ItemType", "")
track = None
video = None
if media_type in JELLYFIN_AUDIO_ITEM_TYPES: if media_type in JELLYFIN_AUDIO_ITEM_TYPES:
if not data_dict.get("Provider_musicbrainztrack", None): scrobble = jellyfin_scrobble_track(data_dict, request.user.id)
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)
artist_dict = {
'name': data_dict.get(KEYS["ARTIST_NAME"], None),
'musicbrainz_id': data_dict.get(KEYS["ARTIST_MB_ID"], None),
}
album_dict = {
"name": data_dict.get(KEYS["ALBUM_NAME"], None),
"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"]),
}
# Convert ticks from Jellyfin from microseconds to nanoseconds
# Ain't nobody got time for nanoseconds
track_dict = {
"title": data_dict.get("Name", ""),
"run_time_ticks": data_dict.get(KEYS["RUN_TIME_TICKS"], None)
// 10000,
"run_time": convert_to_seconds(
data_dict.get(KEYS["RUN_TIME"], None)
),
}
track = Track.find_or_create(artist_dict, album_dict, track_dict)
if media_type in JELLYFIN_VIDEO_ITEM_TYPES: if media_type in JELLYFIN_VIDEO_ITEM_TYPES:
if not data_dict.get("Provider_imdb", None): scrobble = jellyfin_scrobble_video(data_dict, request.user.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)
video = Video.find_or_create(data_dict)
# Now we run off a scrobble
jellyfin_data = {
"user_id": request.user.id,
"timestamp": parse(data_dict.get("UtcTimestamp")),
"playback_position_ticks": data_dict.get("PlaybackPositionTicks")
// 10000,
"playback_position": convert_to_seconds(
data_dict.get("PlaybackPosition")
),
"source": "Jellyfin",
"source_id": data_dict.get('MediaSourceId'),
"is_paused": data_dict.get("IsPaused") in TRUTHY_VALUES,
}
scrobble = None
if video:
scrobble = Scrobble.create_or_update_for_video(
video, request.user.id, jellyfin_data
)
if track:
# Prefer Mopidy MD IDs to Jellyfin, so skip if we already have one
if not track.musicbrainz_id:
track.musicbrainz_id = data_dict.get(KEYS["TRACK_MB_ID"], None)
track.save()
scrobble = Scrobble.create_or_update_for_track(
track, request.user.id, jellyfin_data
)
if not scrobble: if not scrobble:
return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({}, status=status.HTTP_400_BAD_REQUEST)

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-01-13 01:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('videos', '0005_alter_video_options_alter_video_unique_together'),
]
operations = [
migrations.AlterField(
model_name='video',
name='year',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -44,7 +44,7 @@ class Video(ScrobblableMixin):
) )
overview = models.TextField(**BNULL) overview = models.TextField(**BNULL)
tagline = models.TextField(**BNULL) tagline = models.TextField(**BNULL)
year = models.IntegerField() year = models.IntegerField(**BNULL)
# TV show specific fields # TV show specific fields
tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL) tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
@ -80,11 +80,6 @@ class Video(ScrobblableMixin):
"title": data_dict.get("Name", ""), "title": data_dict.get("Name", ""),
"imdb_id": data_dict.get("Provider_imdb", None), "imdb_id": data_dict.get("Provider_imdb", None),
"video_type": Video.VideoType.MOVIE, "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", 0) // 10000,
"run_time": convert_to_seconds(data_dict.get("RunTime", "")),
} }
if data_dict.get("ItemType", "") == "Episode": if data_dict.get("ItemType", "") == "Episode":
@ -97,16 +92,27 @@ class Video(ScrobblableMixin):
else: else:
logger.debug(f"Found series {series}") logger.debug(f"Found series {series}")
video_dict['video_type'] = Video.VideoType.TV_EPISODE 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) video, created = cls.objects.get_or_create(**video_dict)
video_extra_dict = {
"year": data_dict.get("Year", ""),
"overview": data_dict.get("Overview", None),
"tagline": data_dict.get("Tagline", None),
"run_time_ticks": data_dict.get("RunTimeTicks", 0) // 10000,
"run_time": convert_to_seconds(data_dict.get("RunTime", "")),
"tv_series_id": series.id,
"tvdb_id": data_dict.get("Provider_tvdb", None),
"tvrage_id": data_dict.get("Provider_tvrage", None),
"episode_number": data_dict.get("EpisodeNumber", ""),
"season_number": data_dict.get("SeasonNumber", ""),
}
if created: if created:
logger.debug(f"Created new video: {video}") logger.debug(f"Created new video: {video}")
for key, value in video_extra_dict.items():
setattr(video, key, value)
video.save()
else: else:
logger.debug(f"Found video {video}") logger.debug(f"Found video {video}")

View File

@ -49,7 +49,7 @@ DELETE_STALE_SCROBBLES = os.getenv("VROBBLER_DELETE_STALE_SCROBBLES", True)
DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", False) DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", False)
VIDEO_BACKOFF_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 15) VIDEO_BACKOFF_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 15)
MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 5) MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 1)
# If you stop waching or listening to a track, how long should we wait before we # 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 # give up on the old scrobble and start a new one? This could also be considered

View File

@ -198,6 +198,7 @@
{{scrobble.media_obj.title}}<br/> {{scrobble.media_obj.title}}<br/>
{% if scrobble.track %}<em>{{scrobble.track.artist}}</em><br/>{% endif %} {% if scrobble.track %}<em>{{scrobble.track.artist}}</em><br/>{% endif %}
{% if scrobble.podcast_episode%}<em>{{scrobble.podcast_episode.podcast}}</em><br/>{% endif %} {% if scrobble.podcast_episode%}<em>{{scrobble.podcast_episode.podcast}}</em><br/>{% endif %}
{% if scrobble.video.tv_series %}<em>{{scrobble.video.tv_series }}</em><br/>{% endif %}
<small>{{scrobble.created|naturaltime}}<br/> <small>{{scrobble.created|naturaltime}}<br/>
from {{scrobble.source}}</small> from {{scrobble.source}}</small>
<div class="progress-bar" style="margin-right:5px;"> <div class="progress-bar" style="margin-right:5px;">