Add podcasts as new media type
This commit is contained in:
@ -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"
|
||||
|
||||
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -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,
|
||||
|
||||
91
vrobbler/apps/scrobbles/scrobblers.py
Normal file
91
vrobbler/apps/scrobbles/scrobblers.py
Normal 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
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user