Browse Source

[moods] Add moods

Colin Powell 9 months ago
parent
commit
f3fc58e2c0

File diff suppressed because it is too large
+ 2 - 0
data/moods.json


+ 5 - 1
vrobbler/apps/boardgames/models.py

@@ -12,8 +12,8 @@ from django.urls import reverse
 from django_extensions.db.models import TimeStampedModel
 from imagekit.models import ImageSpecField
 from imagekit.processors import ResizeToFit
+from scrobbles.dataclasses import BoardGameLogData
 from scrobbles.mixins import ScrobblableMixin
-from vrobbler.apps.boardgames.bgg import lookup_boardgame_id_from_bgg
 
 logger = logging.getLogger(__name__)
 BNULL = {"blank": True, "null": True}
@@ -96,6 +96,10 @@ class BoardGame(ScrobblableMixin):
             "boardgames:boardgame_detail", kwargs={"slug": self.uuid}
         )
 
+    @property
+    def logdata_cls(self):
+        return BoardGameLogData
+
     def primary_image_url(self) -> str:
         url = ""
         if self.cover:

+ 3 - 3
vrobbler/apps/lifeevents/models.py

@@ -2,7 +2,7 @@ from django.apps import apps
 from django.db import models
 from django.urls import reverse
 import pendulum
-from scrobbles.dataclasses import LifeEventMetadata
+from scrobbles.dataclasses import LifeEventLogData
 from scrobbles.mixins import ScrobblableMixin
 
 BNULL = {"blank": True, "null": True}
@@ -22,8 +22,8 @@ class LifeEvent(ScrobblableMixin):
         )
 
     @property
-    def metadata_class(self):
-        return LifeEventMetadata
+    def logdata_cls(self):
+        return LifeEventLogData
 
     @classmethod
     def find_or_create(cls, title: str) -> "LifeEvent":

+ 23 - 0
vrobbler/apps/moods/admin.py

@@ -0,0 +1,23 @@
+from django.contrib import admin
+
+from moods.models import Mood
+from scrobbles.admin import ScrobbleInline
+
+
+class MoodInline(admin.TabularInline):
+    model = Mood
+    extra = 0
+
+
+@admin.register(Mood)
+class MoodAdmin(admin.ModelAdmin):
+    date_hierarchy = "created"
+    list_display = (
+        "uuid",
+        "title",
+    )
+    ordering = ("-created",)
+    search_fields = ("title",)
+    inlines = [
+        ScrobbleInline,
+    ]

+ 5 - 0
vrobbler/apps/moods/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class MoodsConfig(AppConfig):
+    name = "moods"

+ 79 - 0
vrobbler/apps/moods/migrations/0001_initial.py

@@ -0,0 +1,79 @@
+# Generated by Django 4.2.13 on 2024-08-10 20:01
+
+from django.db import migrations, models
+import django_extensions.db.fields
+import taggit.managers
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Mood",
+            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"
+                    ),
+                ),
+                (
+                    "uuid",
+                    models.UUIDField(
+                        blank=True,
+                        default=uuid.uuid4,
+                        editable=False,
+                        null=True,
+                    ),
+                ),
+                (
+                    "title",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    "run_time_seconds",
+                    models.IntegerField(blank=True, null=True),
+                ),
+                (
+                    "run_time_ticks",
+                    models.PositiveBigIntegerField(blank=True, null=True),
+                ),
+                ("description", models.TextField(blank=True, null=True)),
+                (
+                    "genre",
+                    taggit.managers.TaggableManager(
+                        blank=True,
+                        help_text="A comma-separated list of tags.",
+                        through="scrobbles.ObjectWithGenres",
+                        to="scrobbles.Genre",
+                        verbose_name="Tags",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+        ),
+    ]

+ 0 - 0
vrobbler/apps/moods/migrations/__init__.py


+ 28 - 0
vrobbler/apps/moods/models.py

@@ -0,0 +1,28 @@
+import logging
+from uuid import uuid4
+
+from django.contrib.auth import get_user_model
+from django.db import models
+from django.urls import reverse
+from scrobbles.mixins import ScrobblableMixin
+from vrobbler.apps.scrobbles.dataclasses import MoodLogData
+
+logger = logging.getLogger(__name__)
+BNULL = {"blank": True, "null": True}
+User = get_user_model()
+
+
+class Mood(ScrobblableMixin):
+    description = models.TextField(**BNULL)
+
+    def __str__(self):
+        if self.title:
+            return self.title
+        return str(self.uuid)
+
+    def get_absolute_url(self):
+        return reverse("moods:mood-detail", kwargs={"slug": self.uuid})
+
+    @property
+    def logdata_cls(self):
+        return MoodLogData

+ 14 - 0
vrobbler/apps/moods/urls.py

@@ -0,0 +1,14 @@
+from django.urls import path
+from moods import views
+
+app_name = "moods"
+
+
+urlpatterns = [
+    path("mood/", views.MoodListView.as_view(), name="mood-list"),
+    path(
+        "mood/<slug:slug>/",
+        views.MoodDetailView.as_view(),
+        name="mood-detail",
+    ),
+]

+ 12 - 0
vrobbler/apps/moods/views.py

@@ -0,0 +1,12 @@
+from django.db.models import Count
+from django.views import generic
+from moods.models import Mood
+from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
+
+
+class MoodListView(ScrobbleableListView):
+    model = Mood
+
+
+class MoodDetailView(ScrobbleableDetailView):
+    model = Mood

+ 23 - 24
vrobbler/apps/scrobbles/dataclasses.py

@@ -1,3 +1,4 @@
+from functools import cached_property
 import inspect
 import json
 from dataclasses import asdict, dataclass
@@ -10,17 +11,17 @@ from locations.models import GeoLocation
 User = get_user_model()
 
 
-class ScrobbleMetadataEncoder(json.JSONEncoder):
+class ScrobbleLogDataEncoder(json.JSONEncoder):
     def default(self, o):
         return o.__dict__
 
 
-class ScrobbleMetadataDecoder(json.JSONDecoder):
+class ScrobbleLogDataDecoder(json.JSONDecoder):
     def default(self, o):
         return o.__dict__
 
 
-class JSONMetadata(JSONWizard):
+class JSONDataclass(JSONWizard):
     @property
     def asdict(self):
         return asdict(self)
@@ -29,19 +30,9 @@ class JSONMetadata(JSONWizard):
     def json(self):
         return json.dumps(self.asdict)
 
-    @classmethod
-    def from_dict(cls, env):
-        return cls(
-            **{
-                k: v
-                for k, v in env.items()
-                if k in inspect.signature(cls).parameters
-            }
-        )
-
 
 @dataclass
-class BoardGameScore(JSONMetadata):
+class BoardGameScoreLogData(JSONDataclass):
     user_id: Optional[int] = None
     name: Optional[str] = None
     bgg_username: Optional[str] = None
@@ -53,19 +44,22 @@ class BoardGameScore(JSONMetadata):
 
 
 @dataclass
-class BoardGameMetadata(JSONMetadata):
-    players: Optional[list[BoardGameScore]] = None
+class BoardGameLogData(JSONDataclass):
+    players: Optional[list[BoardGameScoreLogData]] = None
+    location: Optional[str] = None
+    geo_location_id: Optional[int] = None
     difficulty: Optional[int] = None
     solo: Optional[bool] = None
     two_handed: Optional[bool] = None
-    location: Optional[str] = None
 
-    def geo_location(self):
-        return GeoLocation.objects.filter(id=self.geo_location_id).first()
+    @cached_property
+    def geo_location(self) -> Optional[GeoLocation]:
+        if self.geo_location_id:
+            return GeoLocation.objects.filter(id=self.geo_location_id).first()
 
 
 @dataclass
-class BookPageMetadata(JSONMetadata):
+class BookPageLogData(JSONDataclass):
     page_number: Optional[int] = None
     end_ts: Optional[int] = None
     start_ts: Optional[int] = None
@@ -73,14 +67,14 @@ class BookPageMetadata(JSONMetadata):
 
 
 @dataclass
-class BookMetadata(JSONMetadata):
+class BookLogData(JSONDataclass):
     koreader_hash: Optional[str]
     pages_read: Optional[int]
-    page_data: Optional[list[BookPageMetadata]]
+    page_data: Optional[list[BookPageLogData]]
 
 
 @dataclass
-class LifeEventMetadata(JSONMetadata):
+class LifeEventLogData(JSONDataclass):
     participant_user_ids: Optional[list[int]] = None
     participant_names: Optional[list[str]] = None
     location: Optional[str] = None
@@ -108,7 +102,12 @@ class LifeEventMetadata(JSONMetadata):
 
 
 @dataclass
-class VideoMetadata(JSONMetadata):
+class MoodLogData(JSONDataclass):
+    reasons: Optional[str]
+
+
+@dataclass
+class VideoMetadata(JSONDataclass):
     title: str
     video_type: str
     run_time_seconds: int

+ 57 - 0
vrobbler/apps/scrobbles/migrations/0057_scrobble_mood_alter_scrobble_log_and_more.py

@@ -0,0 +1,57 @@
+# Generated by Django 4.2.13 on 2024-08-10 20:01
+
+from django.db import migrations, models
+import django.db.models.deletion
+import scrobbles.dataclasses
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("moods", "0001_initial"),
+        ("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="scrobble",
+            name="mood",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.DO_NOTHING,
+                to="moods.mood",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="scrobble",
+            name="log",
+            field=models.JSONField(
+                blank=True,
+                decoder=scrobbles.dataclasses.ScrobbleLogDataDecoder,
+                encoder=scrobbles.dataclasses.ScrobbleLogDataEncoder,
+                null=True,
+            ),
+        ),
+        migrations.AlterField(
+            model_name="scrobble",
+            name="media_type",
+            field=models.CharField(
+                choices=[
+                    ("Video", "Video"),
+                    ("Track", "Track"),
+                    ("PodcastEpisode", "Podcast episode"),
+                    ("SportEvent", "Sport event"),
+                    ("Book", "Book"),
+                    ("VideoGame", "Video game"),
+                    ("BoardGame", "Board game"),
+                    ("GeoLocation", "GeoLocation"),
+                    ("WebPage", "Web Page"),
+                    ("LifeEvent", "Life event"),
+                    ("Mood", "Mood"),
+                ],
+                default="Video",
+                max_length=14,
+            ),
+        ),
+    ]

+ 17 - 29
vrobbler/apps/scrobbles/models.py

@@ -1,4 +1,3 @@
-import pytz
 import calendar
 import datetime
 import logging
@@ -7,6 +6,7 @@ from typing import Iterable, Optional
 from uuid import uuid4
 
 import pendulum
+import pytz
 from boardgames.models import BoardGame
 from books.koreader import process_koreader_sqlite_file
 from books.models import Book
@@ -19,7 +19,9 @@ from django.utils.functional import cached_property
 from django_extensions.db.models import TimeStampedModel
 from imagekit.models import ImageSpecField
 from imagekit.processors import ResizeToFit
+from lifeevents.models import LifeEvent
 from locations.models import GeoLocation
+from moods.models import Mood
 from music.lastfm import LastFM
 from music.models import Artist, Track
 from podcasts.models import PodcastEpisode
@@ -31,6 +33,7 @@ from profiles.utils import (
     start_of_month,
     start_of_week,
 )
+from scrobbles import dataclasses as logdata
 from scrobbles.constants import LONG_PLAY_MEDIA
 from scrobbles.stats import build_charts
 from scrobbles.utils import media_class_to_foreign_key
@@ -38,17 +41,7 @@ from sports.models import SportEvent
 from videogames import retroarch
 from videogames.models import VideoGame
 from videos.models import Series, Video
-from scrobbles.dataclasses import (
-    BoardGameMetadata,
-    BookMetadata,
-    JSONMetadata,
-    LifeEventMetadata,
-    ScrobbleMetadataDecoder,
-    ScrobbleMetadataEncoder,
-    VideoMetadata,
-)
 from webpages.models import WebPage
-from lifeevents.models import LifeEvent
 
 from vrobbler.apps.scrobbles.constants import MEDIA_END_PADDING_SECONDS
 
@@ -492,6 +485,7 @@ class Scrobble(TimeStampedModel):
         GEO_LOCATION = "GeoLocation", "GeoLocation"
         WEBPAGE = "WebPage", "Web Page"
         LIFE_EVENT = "LifeEvent", "Life event"
+        MOOD = "Mood", "Mood"
 
     uuid = models.UUIDField(editable=False, **BNULL)
     video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
@@ -516,6 +510,7 @@ class Scrobble(TimeStampedModel):
     life_event = models.ForeignKey(
         LifeEvent, on_delete=models.DO_NOTHING, **BNULL
     )
+    mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
     media_type = models.CharField(
         max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
     )
@@ -537,8 +532,8 @@ class Scrobble(TimeStampedModel):
     source = models.CharField(max_length=255, **BNULL)
     log = models.JSONField(
         **BNULL,
-        encoder=ScrobbleMetadataEncoder,
-        decoder=ScrobbleMetadataDecoder,
+        encoder=logdata.ScrobbleLogDataEncoder,
+        decoder=logdata.ScrobbleLogDataDecoder,
     )
     timezone = models.CharField(max_length=50, **BNULL)
 
@@ -582,7 +577,8 @@ class Scrobble(TimeStampedModel):
         # Microseconds mess up Django's filtering, and we don't need be that specific
         if self.timestamp:
             self.timestamp = self.timestamp.replace(microsecond=0)
-        self.media_type = self.MediaType(self.media_obj.__class__.__name__)
+        if self.media_obj:
+            self.media_type = self.MediaType(self.media_obj.__class__.__name__)
 
         return super(Scrobble, self).save(*args, **kwargs)
 
@@ -608,25 +604,15 @@ class Scrobble(TimeStampedModel):
                 )
 
     @property
-    def metadata(self) -> Optional[JSONMetadata]:
-        metadata_cls = None
-        if self.media_type == self.MediaType.LIFE_EVENT:
-            metadata_cls = LifeEventMetadata
-        if self.media_type == self.MediaType.BOARD_GAME:
-            metadata_cls = BoardGameMetadata
-        if self.media_type == self.MediaType.VIDEO:
-            metadata_cls = VideoMetadata
-        if self.media_type == self.MediaType.BOOK:
-            metadata_cls = BookMetadata
-
-        if not metadata_cls:
+    def logdata(self) -> dict:
+        if not self.media_obj.logdata_cls:
             logger.warn(
-                f"Media type has no metadata class",
+                f"Media type has no log data class",
                 extra={"media_type": self.media_type, "scrobble_id": self.id},
             )
-            return None
+            return {}
 
-        return metadata_cls.from_dict(self.log)
+        return self.media_obj.logdata_cls.from_dict(self.log)
 
     def redirect_url(self, user_id) -> str:
         user = User.objects.filter(id=user_id).first()
@@ -864,6 +850,8 @@ class Scrobble(TimeStampedModel):
             media_obj = self.web_page
         if self.life_event:
             media_obj = self.life_event
+        if self.mood:
+            media_obj = self.mood
         return media_obj
 
     def __str__(self):

+ 32 - 1
vrobbler/apps/scrobbles/views.py

@@ -8,7 +8,7 @@ from django.apps import apps
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.mixins import LoginRequiredMixin
-from django.db.models import Q
+from django.db.models import Count, Q
 from django.db.models.query import QuerySet
 from django.http import FileResponse, HttpResponseRedirect, JsonResponse
 from django.urls import reverse_lazy
@@ -60,6 +60,37 @@ from scrobbles.utils import (
 logger = logging.getLogger(__name__)
 
 
+class ScrobbleableListView(ListView):
+    model = None
+    paginate_by = 20
+
+    def get_queryset(self):
+        queryset = super().get_queryset()
+        if not self.request.user.is_anonymous:
+            queryset = queryset.annotate(
+                scrobble_count=Count("scrobble"),
+                filter=Q(user=self.request.user),
+            ).order_by("-scrobble_count")
+        else:
+            queryset = queryset.annotate(
+                scrobble_count=Count("scrobble")
+            ).order_by("-scrobble_count")
+
+
+class ScrobbleableDetailView(DetailView):
+    model = None
+    slug_field = "uuid"
+
+    def get_context_data(self, **kwargs):
+        context_data = super().get_context_data(**kwargs)
+        context_data["scrobbles"] = list()
+        if not self.request.user.is_anonymous:
+            context_data["scrobbles"] = self.object.scrobbles(
+                self.request.user
+            )
+        return context_data
+
+
 class RecentScrobbleList(ListView):
     model = Scrobble
 

+ 1 - 0
vrobbler/settings.py

@@ -125,6 +125,7 @@ INSTALLED_APPS = [
     "locations",
     "webpages",
     "lifeevents",
+    "moods",
     "mathfilters",
     "rest_framework",
     "allauth",

+ 2 - 0
vrobbler/urls.py

@@ -13,6 +13,7 @@ from vrobbler.apps.boardgames import urls as boardgame_urls
 from vrobbler.apps.locations import urls as locations_urls
 from vrobbler.apps.lifeevents import urls as lifeevents_urls
 from vrobbler.apps.webpages import urls as webpages_urls
+from vrobbler.apps.moods import urls as moods_urls
 from vrobbler.apps.music.api.views import (
     AlbumViewSet,
     ArtistViewSet,
@@ -73,6 +74,7 @@ urlpatterns = [
     path("", include(webpages_urls, namespace="webpages")),
     path("", include(podcast_urls, namespace="podcasts")),
     path("", include(lifeevents_urls, namespace="life-events")),
+    path("", include(moods_urls, namespace="moods")),
     path("", include(scrobble_urls, namespace="scrobbles")),
     path(
         "", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"

Some files were not shown because too many files changed in this diff