Add podcasts as new media type

This commit is contained in:
2023-01-12 13:52:30 -05:00
parent f435e60b80
commit 8517212d0e
18 changed files with 599 additions and 38 deletions

View File

@ -3,12 +3,13 @@ from django.contrib import admin
from scrobbles.models import Scrobble
@admin.register(Scrobble)
class ScrobbleAdmin(admin.ModelAdmin):
date_hierarchy = "timestamp"
list_display = (
"timestamp",
"video",
"track",
"media_name",
"media_type",
"source",
"playback_position",
"in_progress",
@ -16,5 +17,18 @@ class ScrobbleAdmin(admin.ModelAdmin):
list_filter = ("in_progress", "source", "track__artist")
ordering = ("-timestamp",)
def media_name(self, obj):
if obj.video:
return obj.video
if obj.track:
return obj.track
if obj.podcast_episode:
return obj.podcast_episode
admin.site.register(Scrobble, ScrobbleAdmin)
def media_type(self, obj):
if obj.video:
return "Video"
if obj.track:
return "Track"
if obj.podcast_episode:
return "Podcast"

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.5 on 2023-01-12 17:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('podcasts', '0002_episode_run_time_episode_run_time_ticks_and_more'),
('scrobbles', '0006_scrobble_track_alter_scrobble_video'),
]
operations = [
migrations.AddField(
model_name='scrobble',
name='podcast_episode',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to='podcasts.episode',
),
),
]

View File

@ -1,6 +1,7 @@
import logging
from datetime import timedelta
from typing import Optional
from uuid import uuid4
from django.conf import settings
from django.contrib.auth import get_user_model
@ -8,6 +9,7 @@ from django.db import models
from django.utils import timezone
from django_extensions.db.models import TimeStampedModel
from music.models import Track
from podcasts.models import Episode
from videos.models import Video
logger = logging.getLogger(__name__)
@ -19,9 +21,22 @@ VIDEO_WAIT_PERIOD = getattr(settings, 'VIDEO_WAIT_PERIOD_DAYS')
TRACK_WAIT_PERIOD = getattr(settings, 'MUSIC_WAIT_PERIOD_MINUTES')
class ScrobblableMixin(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
title = models.CharField(max_length=255, **BNULL)
run_time = models.CharField(max_length=8, **BNULL)
run_time_ticks = models.PositiveBigIntegerField(**BNULL)
class Meta:
abstract = True
class Scrobble(TimeStampedModel):
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
podcast_episode = models.ForeignKey(
Episode, on_delete=models.DO_NOTHING, **BNULL
)
user = models.ForeignKey(
User, blank=True, null=True, on_delete=models.DO_NOTHING
)
@ -134,6 +149,28 @@ class Scrobble(TimeStampedModel):
scrobble, backoff, wait_period, scrobble_data
)
@classmethod
def create_or_update_for_podcast_episode(
cls, episode: "Episode", user_id: int, scrobble_data: dict
) -> "Scrobble":
scrobble_data['podcast_episode_id'] = episode.id
scrobble = (
cls.objects.filter(podcast_episode=episode, user_id=user_id)
.order_by('-modified')
.first()
)
logger.debug(
f"Found existing scrobble for podcast {episode}, updating",
{"scrobble_data": scrobble_data},
)
backoff = timezone.now() + timedelta(seconds=TRACK_BACKOFF)
wait_period = timezone.now() + timedelta(minutes=TRACK_WAIT_PERIOD)
return cls.update_or_create(
scrobble, backoff, wait_period, scrobble_data
)
@classmethod
def update_or_create(
cls,

View File

@ -0,0 +1,91 @@
import logging
from typing import Optional
from django.utils import timezone
from music.models import Track
from podcasts.models import Episode
from scrobbles.models import Scrobble
from scrobbles.utils import parse_mopidy_uri
logger = logging.getLogger(__name__)
def scrobble_podcast(data_dict: dict, user_id: Optional[int]) -> Scrobble:
mopidy_uri = data_dict.get("mopidy_uri", "")
parsed_data = parse_mopidy_uri(mopidy_uri)
producer_dict = {"name": data_dict.get("artist")}
podcast_name = data_dict.get("album")
if not podcast_name:
podcast_name = parsed_data.get("podcast_name")
podcast_dict = {"name": podcast_name}
episode_name = data_dict.get("name")
if not episode_name or '.mp3' in episode_name:
episode_name = parsed_data.get("episode_filename")
episode_dict = {
"title": episode_name,
"run_time_ticks": data_dict.get("run_time_ticks"),
"run_time": data_dict.get("run_time"),
"number": parsed_data.get("episode_num"),
"pub_date": parsed_data.get("pub_date"),
"mopidy_uri": mopidy_uri,
}
episode = Episode.find_or_create(podcast_dict, producer_dict, episode_dict)
# Now we run off a scrobble
mopidy_data = {
"user_id": user_id,
"timestamp": timezone.now(),
"source": "Mopidy",
"status": data_dict.get("status"),
}
scrobble = None
if episode:
scrobble = Scrobble.create_or_update_for_podcast_episode(
episode, user_id, mopidy_data
)
return scrobble
def scrobble_track(
data_dict: dict, user_id: Optional[int]
) -> Optional[Scrobble]:
artist_dict = {
"name": data_dict.get("artist", None),
"musicbrainz_id": data_dict.get("musicbrainz_artist_id", None),
}
album_dict = {
"name": data_dict.get("album"),
"musicbrainz_id": data_dict.get("musicbrainz_album_id"),
}
track_dict = {
"title": data_dict.get("name"),
"run_time_ticks": data_dict.get("run_time_ticks"),
"run_time": data_dict.get("run_time"),
}
track = Track.find_or_create(artist_dict, album_dict, track_dict)
# Now we run off a scrobble
mopidy_data = {
"user_id": user_id,
"timestamp": timezone.now(),
"source": "Mopidy",
"status": data_dict.get("status"),
}
scrobble = None
if track:
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
track.save()
scrobble = Scrobble.create_or_update_for_track(
track, user_id, mopidy_data
)
return scrobble

View File

@ -1,3 +1,11 @@
import logging
from typing import Any
from dateutil.parser import ParserError, parse
logger = logging.getLogger(__name__)
def convert_to_seconds(run_time: str) -> int:
"""Jellyfin sends run time as 00:00:00 string. We want the run time to
actually be in seconds so we'll convert it"""
@ -5,3 +13,46 @@ def convert_to_seconds(run_time: str) -> int:
run_time_list = run_time.split(":")
run_time = (int(run_time_list[1]) * 60) + int(run_time_list[2])
return int(run_time)
def parse_mopidy_uri(uri: str) -> dict:
logger.debug(f"Parsing URI: {uri}")
parsed_uri = uri.split('/')
episode_str = parsed_uri.pop(-1).strip(".mp3")
podcast_str = parsed_uri.pop(-1).replace("%20", " ")
possible_date_str = episode_str[0:10]
try:
pub_date = parse(possible_date_str)
except ParserError:
pub_date = ""
logger.debug(f"Found pub date {pub_date} from Mopidy URI")
try:
if pub_date:
episode_num = int(episode_str.split('-')[3])
else:
episode_num = int(episode_str.split('-')[0])
except IndexError:
episode_num = None
except ValueError:
episode_num = None
logger.debug(f"Found episode num {episode_num} from Mopidy URI")
if pub_date:
episode_str = episode_str.strip(episode_str[:11])
if type(episode_num) is int:
episode_num_gap = len(str(episode_num)) + 1
episode_str = episode_str.strip(episode_str[:episode_num_gap])
episode_str = episode_str.replace('-', ' ')
logger.debug(f"Found episode name {episode_str} from Mopidy URI")
return {
'episode_filename': episode_str,
'episode_num': episode_num,
'podcast_name': podcast_str,
'pub_date': pub_date,
}

View File

@ -27,6 +27,7 @@ from vrobbler.apps.music.aggregators import (
top_tracks,
week_of_scrobbles,
)
from scrobbles.scrobblers import scrobble_podcast, scrobble_track
logger = logging.getLogger(__name__)
@ -187,41 +188,12 @@ def mopidy_websocket(request):
# 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}")
artist_dict = {
"name": data_dict.get("artist", None),
"musicbrainz_id": data_dict.get("musicbrainz_artist_id", None),
}
album_dict = {
"name": data_dict.get("album"),
"musicbrainz_id": data_dict.get("musicbrainz_album_id"),
}
track_dict = {
"title": data_dict.get("name"),
"run_time_ticks": data_dict.get("run_time_ticks"),
"run_time": data_dict.get("run_time"),
}
track = Track.find_or_create(artist_dict, album_dict, track_dict)
# Now we run off a scrobble
mopidy_data = {
"user_id": request.user.id,
"timestamp": timezone.now(),
"source": "Mopidy",
"status": data_dict.get("status"),
}
scrobble = None
if track:
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
track.save()
scrobble = Scrobble.create_or_update_for_track(
track, request.user.id, mopidy_data
)
if 'podcast' in data_dict.get('mopidy_uri'):
scrobble = scrobble_podcast(data_dict, request.user.id)
else:
scrobble = scrobble_track(data_dict, request.user.id)
if not scrobble:
return Response({}, status=status.HTTP_400_BAD_REQUEST)