Add podcasts as new media type
This commit is contained in:
18
vrobbler/apps/music/migrations/0007_alter_album_artists.py
Normal file
18
vrobbler/apps/music/migrations/0007_alter_album_artists.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-12 17:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('music', '0006_album_artists'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='album',
|
||||||
|
name='artists',
|
||||||
|
field=models.ManyToManyField(to='music.artist'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -32,7 +32,7 @@ class Artist(TimeStampedModel):
|
|||||||
class Album(TimeStampedModel):
|
class Album(TimeStampedModel):
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
artists = models.ManyToManyField(Artist, **BNULL)
|
artists = models.ManyToManyField(Artist)
|
||||||
year = models.IntegerField(**BNULL)
|
year = models.IntegerField(**BNULL)
|
||||||
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
|
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
|
||||||
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
|
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
|
||||||
@ -124,6 +124,7 @@ class Track(TimeStampedModel):
|
|||||||
f"No artist or artist musicbrainz ID found in message from source, not scrobbling"
|
f"No artist or artist musicbrainz ID found in message from source, not scrobbling"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
artist, artist_created = Artist.objects.get_or_create(**artist_dict)
|
artist, artist_created = Artist.objects.get_or_create(**artist_dict)
|
||||||
if artist_created:
|
if artist_created:
|
||||||
logger.debug(f"Created new album {artist}")
|
logger.debug(f"Created new album {artist}")
|
||||||
|
|||||||
0
vrobbler/apps/podcasts/__init__.py
Normal file
0
vrobbler/apps/podcasts/__init__.py
Normal file
33
vrobbler/apps/podcasts/admin.py
Normal file
33
vrobbler/apps/podcasts/admin.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from django.contrib import admin
|
||||||
|
from podcasts.models import Episode, Podcast, Producer
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Producer)
|
||||||
|
class ProducerAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = "created"
|
||||||
|
list_display = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Podcast)
|
||||||
|
class PodcastAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = "created"
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"producer",
|
||||||
|
"active",
|
||||||
|
)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Episode)
|
||||||
|
class EpisodeAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = "created"
|
||||||
|
list_display = (
|
||||||
|
"title",
|
||||||
|
"podcast",
|
||||||
|
"run_time",
|
||||||
|
)
|
||||||
|
list_filter = ("podcast",)
|
||||||
|
ordering = ("-created",)
|
||||||
5
vrobbler/apps/podcasts/apps.py
Normal file
5
vrobbler/apps/podcasts/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PodcastsConfig(AppConfig):
|
||||||
|
name = 'podcasts'
|
||||||
158
vrobbler/apps/podcasts/migrations/0001_initial.py
Normal file
158
vrobbler/apps/podcasts/migrations/0001_initial.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-12 17:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_extensions.db.fields
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Producer',
|
||||||
|
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)),
|
||||||
|
(
|
||||||
|
'uuid',
|
||||||
|
models.UUIDField(
|
||||||
|
blank=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'modified',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Podcast',
|
||||||
|
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)),
|
||||||
|
(
|
||||||
|
'uuid',
|
||||||
|
models.UUIDField(
|
||||||
|
blank=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
('active', models.BooleanField(default=True)),
|
||||||
|
('url', models.URLField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
'producer',
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||||
|
to='podcasts.producer',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'modified',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Episode',
|
||||||
|
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(max_length=255)),
|
||||||
|
(
|
||||||
|
'uuid',
|
||||||
|
models.UUIDField(
|
||||||
|
blank=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'mopidy_uri',
|
||||||
|
models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'podcast',
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||||
|
to='podcasts.producer',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'modified',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
# 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', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='episode',
|
||||||
|
name='run_time',
|
||||||
|
field=models.CharField(blank=True, max_length=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='episode',
|
||||||
|
name='run_time_ticks',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='episode',
|
||||||
|
name='podcast',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||||
|
to='podcasts.podcast',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
vrobbler/apps/podcasts/migrations/0003_episode_pub_date.py
Normal file
18
vrobbler/apps/podcasts/migrations/0003_episode_pub_date.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-12 18:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('podcasts', '0002_episode_run_time_episode_run_time_ticks_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='episode',
|
||||||
|
name='pub_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
vrobbler/apps/podcasts/migrations/0004_episode_number.py
Normal file
18
vrobbler/apps/podcasts/migrations/0004_episode_number.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-12 18:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('podcasts', '0003_episode_pub_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='episode',
|
||||||
|
name='number',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
vrobbler/apps/podcasts/migrations/__init__.py
Normal file
0
vrobbler/apps/podcasts/migrations/__init__.py
Normal file
87
vrobbler/apps/podcasts/models.py
Normal file
87
vrobbler/apps/podcasts/models.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_extensions.db.models import TimeStampedModel
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
BNULL = {"blank": True, "null": True}
|
||||||
|
|
||||||
|
|
||||||
|
class Producer(TimeStampedModel):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Podcast(TimeStampedModel):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
|
producer = models.ForeignKey(
|
||||||
|
Producer, on_delete=models.DO_NOTHING, **BNULL
|
||||||
|
)
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
|
url = models.URLField(**BNULL)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Episode(TimeStampedModel):
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
|
podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)
|
||||||
|
number = models.IntegerField(**BNULL)
|
||||||
|
pub_date = models.DateField(**BNULL)
|
||||||
|
run_time = models.CharField(max_length=8, **BNULL)
|
||||||
|
run_time_ticks = models.PositiveBigIntegerField(**BNULL)
|
||||||
|
mopidy_uri = models.CharField(max_length=255, **BNULL)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.title}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_or_create(
|
||||||
|
cls, podcast_dict: Dict, producer_dict: Dict, episode_dict: Dict
|
||||||
|
) -> Optional["Episode"]:
|
||||||
|
"""Given a data dict from Mopidy, finds or creates a podcast and
|
||||||
|
producer before saving the epsiode so it can be scrobbled.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not podcast_dict.get('name'):
|
||||||
|
logger.warning(f"No name from source for podcast, not scrobbling")
|
||||||
|
return
|
||||||
|
|
||||||
|
producer = None
|
||||||
|
if producer_dict.get('name'):
|
||||||
|
producer, producer_created = Producer.objects.get_or_create(
|
||||||
|
**producer_dict
|
||||||
|
)
|
||||||
|
if producer_created:
|
||||||
|
logger.debug(f"Created new producer {producer}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Found producer {producer}")
|
||||||
|
|
||||||
|
if producer:
|
||||||
|
podcast_dict["producer_id"] = producer.id
|
||||||
|
podcast, podcast_created = Podcast.objects.get_or_create(
|
||||||
|
**podcast_dict
|
||||||
|
)
|
||||||
|
if podcast_created:
|
||||||
|
logger.debug(f"Created new podcast {podcast}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Found podcast {podcast}")
|
||||||
|
|
||||||
|
episode_dict['podcast_id'] = podcast.id
|
||||||
|
|
||||||
|
episode, created = cls.objects.get_or_create(**episode_dict)
|
||||||
|
if created:
|
||||||
|
logger.debug(f"Created new episode: {episode}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Found episode {episode}")
|
||||||
|
|
||||||
|
return episode
|
||||||
@ -3,12 +3,13 @@ from django.contrib import admin
|
|||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Scrobble)
|
||||||
class ScrobbleAdmin(admin.ModelAdmin):
|
class ScrobbleAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "timestamp"
|
date_hierarchy = "timestamp"
|
||||||
list_display = (
|
list_display = (
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"video",
|
"media_name",
|
||||||
"track",
|
"media_type",
|
||||||
"source",
|
"source",
|
||||||
"playback_position",
|
"playback_position",
|
||||||
"in_progress",
|
"in_progress",
|
||||||
@ -16,5 +17,18 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ("in_progress", "source", "track__artist")
|
list_filter = ("in_progress", "source", "track__artist")
|
||||||
ordering = ("-timestamp",)
|
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
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@ -8,6 +9,7 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from music.models import Track
|
from music.models import Track
|
||||||
|
from podcasts.models import Episode
|
||||||
from videos.models import Video
|
from videos.models import Video
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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')
|
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):
|
class Scrobble(TimeStampedModel):
|
||||||
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
||||||
track = models.ForeignKey(Track, 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 = models.ForeignKey(
|
||||||
User, blank=True, null=True, on_delete=models.DO_NOTHING
|
User, blank=True, null=True, on_delete=models.DO_NOTHING
|
||||||
)
|
)
|
||||||
@ -134,6 +149,28 @@ class Scrobble(TimeStampedModel):
|
|||||||
scrobble, backoff, wait_period, scrobble_data
|
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
|
@classmethod
|
||||||
def update_or_create(
|
def update_or_create(
|
||||||
cls,
|
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:
|
def convert_to_seconds(run_time: str) -> int:
|
||||||
"""Jellyfin sends run time as 00:00:00 string. We want the run time to
|
"""Jellyfin sends run time as 00:00:00 string. We want the run time to
|
||||||
actually be in seconds so we'll convert it"""
|
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_list = run_time.split(":")
|
||||||
run_time = (int(run_time_list[1]) * 60) + int(run_time_list[2])
|
run_time = (int(run_time_list[1]) * 60) + int(run_time_list[2])
|
||||||
return int(run_time)
|
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,
|
top_tracks,
|
||||||
week_of_scrobbles,
|
week_of_scrobbles,
|
||||||
)
|
)
|
||||||
|
from scrobbles.scrobblers import scrobble_podcast, scrobble_track
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -187,41 +188,12 @@ def mopidy_websocket(request):
|
|||||||
# For making things easier to build new input processors
|
# For making things easier to build new input processors
|
||||||
if getattr(settings, "DUMP_REQUEST_DATA", False):
|
if getattr(settings, "DUMP_REQUEST_DATA", False):
|
||||||
json_data = json.dumps(data_dict, indent=4)
|
json_data = json.dumps(data_dict, indent=4)
|
||||||
|
logger.debug(f"{json_data}")
|
||||||
|
|
||||||
artist_dict = {
|
if 'podcast' in data_dict.get('mopidy_uri'):
|
||||||
"name": data_dict.get("artist", None),
|
scrobble = scrobble_podcast(data_dict, request.user.id)
|
||||||
"musicbrainz_id": data_dict.get("musicbrainz_artist_id", None),
|
else:
|
||||||
}
|
scrobble = scrobble_track(data_dict, request.user.id)
|
||||||
|
|
||||||
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 not scrobble:
|
if not scrobble:
|
||||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|||||||
@ -86,6 +86,7 @@ INSTALLED_APPS = [
|
|||||||
"scrobbles",
|
"scrobbles",
|
||||||
"videos",
|
"videos",
|
||||||
"music",
|
"music",
|
||||||
|
"podcasts",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
|
|||||||
Reference in New Issue
Block a user