[moods] Add moods

This commit is contained in:
2024-08-10 22:12:24 -04:00
parent b02b75fa90
commit f3fc58e2c0
16 changed files with 304 additions and 58 deletions

3
data/moods.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -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:

View File

@ -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":

View File

@ -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,
]

View File

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

View File

@ -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,
},
),
]

View File

@ -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

View File

@ -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",
),
]

View File

@ -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

View File

@ -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

View File

@ -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,
),
),
]

View File

@ -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):

View File

@ -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

View File

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

View File

@ -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"