From c9874b3fdaea55ec232346ce80fec97df07925bc Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Tue, 14 Mar 2023 18:35:58 -0400 Subject: [PATCH] Add genres! --- .../apps/books/migrations/0011_book_genre.py | 25 +++++ .../apps/music/migrations/0017_track_genre.py | 25 +++++ .../podcasts/migrations/0008_episode_genre.py | 25 +++++ vrobbler/apps/scrobbles/admin.py | 9 ++ .../migrations/0033_genre_objectwithgenres.py | 92 +++++++++++++++++++ vrobbler/apps/scrobbles/mixins.py | 20 ++++ vrobbler/apps/scrobbles/models.py | 1 - .../migrations/0013_sportevent_genre.py | 25 +++++ .../migrations/0009_videogame_genre.py | 25 +++++ ...res_series_tags_alter_video_cover_image.py | 28 ++++++ .../apps/videos/migrations/0011_video_tags.py | 25 +++++ ...remove_video_tags_series_genre_and_more.py | 43 +++++++++ vrobbler/apps/videos/models.py | 16 +++- 13 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 vrobbler/apps/books/migrations/0011_book_genre.py create mode 100644 vrobbler/apps/music/migrations/0017_track_genre.py create mode 100644 vrobbler/apps/podcasts/migrations/0008_episode_genre.py create mode 100644 vrobbler/apps/scrobbles/migrations/0033_genre_objectwithgenres.py create mode 100644 vrobbler/apps/sports/migrations/0013_sportevent_genre.py create mode 100644 vrobbler/apps/videogames/migrations/0009_videogame_genre.py create mode 100644 vrobbler/apps/videos/migrations/0010_rename_genres_series_tags_alter_video_cover_image.py create mode 100644 vrobbler/apps/videos/migrations/0011_video_tags.py create mode 100644 vrobbler/apps/videos/migrations/0012_remove_series_tags_remove_video_tags_series_genre_and_more.py diff --git a/vrobbler/apps/books/migrations/0011_book_genre.py b/vrobbler/apps/books/migrations/0011_book_genre.py new file mode 100644 index 0000000..f17f6bc --- /dev/null +++ b/vrobbler/apps/books/migrations/0011_book_genre.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0033_genre_objectwithgenres"), + ("books", "0010_rename_run_time_book_run_time_seconds"), + ] + + operations = [ + migrations.AddField( + model_name="book", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/music/migrations/0017_track_genre.py b/vrobbler/apps/music/migrations/0017_track_genre.py new file mode 100644 index 0000000..f88d56d --- /dev/null +++ b/vrobbler/apps/music/migrations/0017_track_genre.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0033_genre_objectwithgenres"), + ("music", "0016_rename_run_time_track_run_time_seconds"), + ] + + operations = [ + migrations.AddField( + model_name="track", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/podcasts/migrations/0008_episode_genre.py b/vrobbler/apps/podcasts/migrations/0008_episode_genre.py new file mode 100644 index 0000000..e8bbd64 --- /dev/null +++ b/vrobbler/apps/podcasts/migrations/0008_episode_genre.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0033_genre_objectwithgenres"), + ("podcasts", "0007_rename_run_time_episode_run_time_seconds"), + ] + + operations = [ + migrations.AddField( + model_name="episode", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/scrobbles/admin.py b/vrobbler/apps/scrobbles/admin.py index 599c046..471251b 100644 --- a/vrobbler/apps/scrobbles/admin.py +++ b/vrobbler/apps/scrobbles/admin.py @@ -6,6 +6,7 @@ from scrobbles.models import ( LastFmImport, Scrobble, ) +from scrobbles.mixins import Genre class ScrobbleInline(admin.TabularInline): @@ -49,6 +50,14 @@ class KoReaderImportAdmin(ImportBaseAdmin): """""" +@admin.register(Genre) +class GenreAdmin(admin.ModelAdmin): + list_display = ( + "name", + "source", + ) + + @admin.register(ChartRecord) class ChartRecordAdmin(admin.ModelAdmin): date_hierarchy = "created" diff --git a/vrobbler/apps/scrobbles/migrations/0033_genre_objectwithgenres.py b/vrobbler/apps/scrobbles/migrations/0033_genre_objectwithgenres.py new file mode 100644 index 0000000..6796698 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0033_genre_objectwithgenres.py @@ -0,0 +1,92 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ( + "scrobbles", + "0032_rename_playback_position_scrobble_playback_position_seconds", + ), + ] + + operations = [ + migrations.CreateModel( + name="Genre", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=100, unique=True, verbose_name="name" + ), + ), + ( + "slug", + models.SlugField( + max_length=100, unique=True, verbose_name="slug" + ), + ), + ( + "source", + models.CharField(blank=True, max_length=255, null=True), + ), + ], + options={ + "verbose_name": "Genre", + "verbose_name_plural": "Genres", + }, + ), + migrations.CreateModel( + name="ObjectWithGenres", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "object_id", + models.IntegerField( + db_index=True, verbose_name="object ID" + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s_tagged_items", + to="contenttypes.contenttype", + verbose_name="content type", + ), + ), + ( + "tag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s_items", + to="scrobbles.genre", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/vrobbler/apps/scrobbles/mixins.py b/vrobbler/apps/scrobbles/mixins.py index 93ed675..22b6ab1 100644 --- a/vrobbler/apps/scrobbles/mixins.py +++ b/vrobbler/apps/scrobbles/mixins.py @@ -5,13 +5,31 @@ from uuid import uuid4 from django.db import models from django.urls import reverse from django_extensions.db.models import TimeStampedModel +from taggit.managers import TaggableManager from scrobbles.utils import get_scrobbles_for_media +from taggit.models import TagBase, GenericTaggedItemBase BNULL = {"blank": True, "null": True} logger = logging.getLogger(__name__) +class Genre(TagBase): + source = models.CharField(max_length=255, **BNULL) + + class Meta: + verbose_name = "Genre" + verbose_name_plural = "Genres" + + +class ObjectWithGenres(GenericTaggedItemBase): + tag = models.ForeignKey( + Genre, + on_delete=models.CASCADE, + related_name="%(app_label)s_%(class)s_items", + ) + + class ScrobblableMixin(TimeStampedModel): SECONDS_TO_STALE = 1600 @@ -20,6 +38,8 @@ class ScrobblableMixin(TimeStampedModel): run_time_seconds = models.IntegerField(**BNULL) run_time_ticks = models.PositiveBigIntegerField(**BNULL) + genre = TaggableManager(through=ObjectWithGenres) + class Meta: abstract = True diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 3d6aee3..cee385b 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -5,7 +5,6 @@ from typing import Optional from uuid import uuid4 from books.models import Book -from django.apps import apps from django.contrib.auth import get_user_model from django.db import models from django.urls import reverse diff --git a/vrobbler/apps/sports/migrations/0013_sportevent_genre.py b/vrobbler/apps/sports/migrations/0013_sportevent_genre.py new file mode 100644 index 0000000..241bc32 --- /dev/null +++ b/vrobbler/apps/sports/migrations/0013_sportevent_genre.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0033_genre_objectwithgenres"), + ("sports", "0012_sport_default_event_run_time_seconds"), + ] + + operations = [ + migrations.AddField( + model_name="sportevent", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/videogames/migrations/0009_videogame_genre.py b/vrobbler/apps/videogames/migrations/0009_videogame_genre.py new file mode 100644 index 0000000..d34dbdc --- /dev/null +++ b/vrobbler/apps/videogames/migrations/0009_videogame_genre.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0033_genre_objectwithgenres"), + ("videogames", "0008_rename_run_time_videogame_run_time_seconds"), + ] + + operations = [ + migrations.AddField( + model_name="videogame", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/videos/migrations/0010_rename_genres_series_tags_alter_video_cover_image.py b/vrobbler/apps/videos/migrations/0010_rename_genres_series_tags_alter_video_cover_image.py new file mode 100644 index 0000000..c7cdb88 --- /dev/null +++ b/vrobbler/apps/videos/migrations/0010_rename_genres_series_tags_alter_video_cover_image.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.5 on 2023-03-14 19:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "videos", + "0009_rename_overview_series_plot_remove_series_tagline_and_more", + ), + ] + + operations = [ + migrations.RenameField( + model_name="series", + old_name="genres", + new_name="tags", + ), + migrations.AlterField( + model_name="video", + name="cover_image", + field=models.ImageField( + blank=True, null=True, upload_to="videos/video/" + ), + ), + ] diff --git a/vrobbler/apps/videos/migrations/0011_video_tags.py b/vrobbler/apps/videos/migrations/0011_video_tags.py new file mode 100644 index 0000000..fcd8461 --- /dev/null +++ b/vrobbler/apps/videos/migrations/0011_video_tags.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2023-03-14 19:42 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("taggit", "0004_alter_taggeditem_content_type_alter_taggeditem_tag"), + ("videos", "0010_rename_genres_series_tags_alter_video_cover_image"), + ] + + operations = [ + migrations.AddField( + model_name="video", + name="tags", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/videos/migrations/0012_remove_series_tags_remove_video_tags_series_genre_and_more.py b/vrobbler/apps/videos/migrations/0012_remove_series_tags_remove_video_tags_series_genre_and_more.py new file mode 100644 index 0000000..b6a792b --- /dev/null +++ b/vrobbler/apps/videos/migrations/0012_remove_series_tags_remove_video_tags_series_genre_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.5 on 2023-03-14 22:27 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0033_genre_objectwithgenres"), + ("videos", "0011_video_tags"), + ] + + operations = [ + migrations.RemoveField( + model_name="series", + name="tags", + ), + migrations.RemoveField( + model_name="video", + name="tags", + ), + migrations.AddField( + model_name="series", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + migrations.AddField( + model_name="video", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/videos/models.py b/vrobbler/apps/videos/models.py index 33ae94d..a1bda8c 100644 --- a/vrobbler/apps/videos/models.py +++ b/vrobbler/apps/videos/models.py @@ -9,8 +9,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django_extensions.db.models import TimeStampedModel -from scrobbles.mixins import ScrobblableMixin -from scrobbles.utils import convert_to_seconds +from scrobbles.mixins import ObjectWithGenres, ScrobblableMixin from taggit.managers import TaggableManager from videos.imdb import lookup_video_from_imdb @@ -27,7 +26,7 @@ class Series(TimeStampedModel): imdb_rating = models.FloatField(**BNULL) cover_image = models.ImageField(upload_to="videos/series/", **BNULL) - genres = TaggableManager() + genre = TaggableManager(through=ObjectWithGenres) class Meta: verbose_name_plural = "series" @@ -49,7 +48,10 @@ class Series(TimeStampedModel): ).order_by("-timestamp") def fix_metadata(self, force_update=False): - imdb_dict = lookup_video_from_imdb(self.name, kind="tv series") + name_or_id = self.name + if self.imdb_id: + name_or_id = self.imdb_id + imdb_dict = lookup_video_from_imdb(name_or_id) if not imdb_dict: logger.warn(f"No imdb data for {self}") return @@ -67,6 +69,9 @@ class Series(TimeStampedModel): fname = f"{self.name}_{self.uuid}.jpg" self.cover_image.save(fname, ContentFile(r.content), save=True) + if genres := imdb_dict.data.get("genres"): + self.genre.add(*genres) + class Video(ScrobblableMixin): COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90) @@ -149,6 +154,9 @@ class Video(ScrobblableMixin): fname = f"{self.title}_{self.uuid}.jpg" self.cover_image.save(fname, ContentFile(r.content), save=True) + if genres := imdb_dict.data.get("genres"): + self.genre.add(*genres) + @classmethod def find_or_create(cls, data_dict: Dict) -> "Video": """Given a data dict from Jellyfin, does the heavy lifting of looking up