[scrobbles] Fix mood starting, and clean up code
This commit is contained in:
18
vrobbler/apps/moods/migrations/0002_mood_image.py
Normal file
18
vrobbler/apps/moods/migrations/0002_mood_image.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.13 on 2024-08-10 20:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("moods", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="mood",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to="moods/"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from imagekit.models import ImageSpecField
|
||||||
|
from imagekit.processors import ResizeToFit
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import ScrobblableMixin
|
||||||
|
|
||||||
from vrobbler.apps.scrobbles.dataclasses import MoodLogData
|
from vrobbler.apps.scrobbles.dataclasses import MoodLogData
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -14,6 +16,19 @@ User = get_user_model()
|
|||||||
|
|
||||||
class Mood(ScrobblableMixin):
|
class Mood(ScrobblableMixin):
|
||||||
description = models.TextField(**BNULL)
|
description = models.TextField(**BNULL)
|
||||||
|
image = models.ImageField(upload_to="moods/", **BNULL)
|
||||||
|
image_small = ImageSpecField(
|
||||||
|
source="thumbnail",
|
||||||
|
processors=[ResizeToFit(100, 100)],
|
||||||
|
format="JPEG",
|
||||||
|
options={"quality": 60},
|
||||||
|
)
|
||||||
|
image_medium = ImageSpecField(
|
||||||
|
source="thumbnail",
|
||||||
|
processors=[ResizeToFit(300, 300)],
|
||||||
|
format="JPEG",
|
||||||
|
options={"quality": 75},
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.title:
|
if self.title:
|
||||||
@ -23,6 +38,19 @@ class Mood(ScrobblableMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("moods:mood-detail", kwargs={"slug": self.uuid})
|
return reverse("moods:mood-detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
|
def get_start_url(self):
|
||||||
|
return reverse("scrobbles:start", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def logdata_cls(self):
|
def logdata_cls(self):
|
||||||
return MoodLogData
|
return MoodLogData
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primary_image_url(self) -> str:
|
||||||
|
if self.image:
|
||||||
|
return self.image.url
|
||||||
|
return ""
|
||||||
|
|||||||
@ -12,6 +12,7 @@ PLAY_AGAIN_MEDIA = {
|
|||||||
"videogames": "VideoGame",
|
"videogames": "VideoGame",
|
||||||
"books": "Book",
|
"books": "Book",
|
||||||
"boardgames": "BoardGame",
|
"boardgames": "BoardGame",
|
||||||
|
"moods": "Mood",
|
||||||
}
|
}
|
||||||
|
|
||||||
MEDIA_END_PADDING_SECONDS = {
|
MEDIA_END_PADDING_SECONDS = {
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import logging
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from taggit.managers import TaggableManager
|
|
||||||
from scrobbles.utils import get_scrobbles_for_media
|
from scrobbles.utils import get_scrobbles_for_media
|
||||||
from taggit.models import TagBase, GenericTaggedItemBase
|
from taggit.managers import TaggableManager
|
||||||
|
from taggit.models import GenericTaggedItemBase, TagBase
|
||||||
|
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
|
|
||||||
@ -33,6 +35,7 @@ class ObjectWithGenres(GenericTaggedItemBase):
|
|||||||
|
|
||||||
class ScrobblableMixin(TimeStampedModel):
|
class ScrobblableMixin(TimeStampedModel):
|
||||||
SECONDS_TO_STALE = 1600
|
SECONDS_TO_STALE = 1600
|
||||||
|
COMPLETION_PERCENT = 100
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
title = models.CharField(max_length=255, **BNULL)
|
title = models.CharField(max_length=255, **BNULL)
|
||||||
@ -44,21 +47,53 @@ class ScrobblableMixin(TimeStampedModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
def basic_scrobble_data(self, user_id) -> dict:
|
||||||
|
return {
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": timezone.now(),
|
||||||
|
"playback_position_seconds": 0,
|
||||||
|
"source": "Vrobbler",
|
||||||
|
}
|
||||||
|
|
||||||
|
def scrobble_for_user(self, user_id):
|
||||||
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
|
scrobble_data = self.basic_scrobble_data(user_id)
|
||||||
|
logger.info(
|
||||||
|
"[scrobble_for_user] called",
|
||||||
|
extra={
|
||||||
|
"webpage_id": self.id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"scrobble_data": scrobble_data,
|
||||||
|
"media_type": Scrobble.MediaType.WEBPAGE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return Scrobble.create_or_update(self, user_id, scrobble_data)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def primary_image_url(self) -> str:
|
def primary_image_url(self) -> str:
|
||||||
logger.warn(f"Not implemented yet")
|
logger.warn(f"Not implemented yet")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def fix_metadata(self):
|
@property
|
||||||
|
def logdata_cls(self) -> None:
|
||||||
|
logger.warn("logdata_cls() not implemented yet")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def fix_metadata(self) -> None:
|
||||||
logger.warn("fix_metadata() not implemented yet")
|
logger.warn("fix_metadata() not implemented yet")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls):
|
def find_or_create(cls) -> None:
|
||||||
logger.warn("find_or_create() not implemented yet")
|
logger.warn("find_or_create() not implemented yet")
|
||||||
|
|
||||||
def scrobble(self, user_id, **kwargs):
|
def scrobble(self, user_id, **kwargs):
|
||||||
"""Given a user ID and a dictionary of data, attempts to scrobble it"""
|
"""Given a user ID and a dictionary of data, attempts to scrobble it"""
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
Scrobble.create_or_update(self, user_id, **kwargs)
|
Scrobble.create_or_update(self, user_id, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -904,7 +904,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
.order_by("-timestamp")
|
.order_by("-timestamp")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
source = scrobble_data.get("source")
|
source = scrobble_data.get("source", "Vrobbler")
|
||||||
mtype = media.__class__.__name__
|
mtype = media.__class__.__name__
|
||||||
mopidy_status = scrobble_data.get("mopidy_status", None)
|
mopidy_status = scrobble_data.get("mopidy_status", None)
|
||||||
|
|
||||||
|
|||||||
@ -69,12 +69,13 @@ class ScrobbleableListView(ListView):
|
|||||||
if not self.request.user.is_anonymous:
|
if not self.request.user.is_anonymous:
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
scrobble_count=Count("scrobble"),
|
scrobble_count=Count("scrobble"),
|
||||||
filter=Q(user=self.request.user),
|
filter=Q(scrobble__user=self.request.user),
|
||||||
).order_by("-scrobble_count")
|
).order_by("-scrobble_count")
|
||||||
else:
|
else:
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
scrobble_count=Count("scrobble")
|
scrobble_count=Count("scrobble")
|
||||||
).order_by("-scrobble_count")
|
).order_by("-scrobble_count")
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleableDetailView(DetailView):
|
class ScrobbleableDetailView(DetailView):
|
||||||
@ -85,8 +86,8 @@ class ScrobbleableDetailView(DetailView):
|
|||||||
context_data = super().get_context_data(**kwargs)
|
context_data = super().get_context_data(**kwargs)
|
||||||
context_data["scrobbles"] = list()
|
context_data["scrobbles"] = list()
|
||||||
if not self.request.user.is_anonymous:
|
if not self.request.user.is_anonymous:
|
||||||
context_data["scrobbles"] = self.object.scrobbles(
|
context_data["scrobbles"] = self.object.scrobble_set.filter(
|
||||||
self.request.user
|
user=self.request.user
|
||||||
)
|
)
|
||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
@ -429,6 +430,10 @@ def import_audioscrobbler_file(request):
|
|||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def scrobble_start(request, uuid):
|
def scrobble_start(request, uuid):
|
||||||
|
logger.info(
|
||||||
|
"[scrobble_start] called",
|
||||||
|
extra={"request": request, "uuid": uuid},
|
||||||
|
)
|
||||||
user = request.user
|
user = request.user
|
||||||
success_url = request.META.get("HTTP_REFERER")
|
success_url = request.META.get("HTTP_REFERER")
|
||||||
|
|
||||||
@ -443,19 +448,17 @@ def scrobble_start(request, uuid):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not media_obj:
|
if not media_obj:
|
||||||
|
logger.info(
|
||||||
|
"[scrobble_start] media object not found",
|
||||||
|
extra={"uuid": uuid, "user_id": user.id},
|
||||||
|
)
|
||||||
|
# TODO Log that we couldn't find a media obj to scrobble
|
||||||
return
|
return
|
||||||
|
|
||||||
scrobble = None
|
scrobble = None
|
||||||
user_id = request.user.id
|
user_id = request.user.id
|
||||||
if media_obj:
|
if media_obj:
|
||||||
if media_obj.__class__.__name__ == Scrobble.MediaType.BOOK:
|
media_obj.scrobble_for_user(user_id)
|
||||||
scrobble = manual_scrobble_book(media_obj.openlibrary_id, user_id)
|
|
||||||
if media_obj.__class__.__name__ == Scrobble.MediaType.VIDEO_GAME:
|
|
||||||
scrobble = manual_scrobble_video_game(media_obj.hltb_id, user_id)
|
|
||||||
if media_obj.__class__.__name__ == Scrobble.MediaType.BOARD_GAME:
|
|
||||||
scrobble = manual_scrobble_board_game(media_obj.bggeek_id, user_id)
|
|
||||||
if media_obj.__class__.__name__ == Scrobble.MediaType.WEBPAGE:
|
|
||||||
scrobble = manual_scrobble_webpage(media_obj.url, user_id)
|
|
||||||
|
|
||||||
if scrobble:
|
if scrobble:
|
||||||
messages.add_message(
|
messages.add_message(
|
||||||
|
|||||||
@ -89,6 +89,28 @@ class WebPage(ScrobblableMixin):
|
|||||||
def subtitle(self):
|
def subtitle(self):
|
||||||
return self.domain
|
return self.domain
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primary_image_url(self) -> str:
|
||||||
|
# TODO Figure out how to add a preview?
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def scrobble_for_user(self, user_id):
|
||||||
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
|
scrobble_data = self.basic_scrobble_data(user_id)
|
||||||
|
logger.info(
|
||||||
|
"[scrobble_for_user] called for webpage",
|
||||||
|
extra={
|
||||||
|
"webpage_id": self.id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"scrobble_data": scrobble_data,
|
||||||
|
"media_type": Scrobble.MediaType.WEBPAGE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
scrobble = Scrobble.create_or_update(self, user_id, scrobble_data)
|
||||||
|
# TODO Possibly make this async?
|
||||||
|
scrobble.push_to_archivebox()
|
||||||
|
return scrobble
|
||||||
|
|
||||||
def scrobbles(self, user):
|
def scrobbles(self, user):
|
||||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
return Scrobble.objects.filter(user=user, web_page=self).order_by(
|
return Scrobble.objects.filter(user=user, web_page=self).order_by(
|
||||||
|
|||||||
39
vrobbler/templates/moods/mood_detail.html
Normal file
39
vrobbler/templates/moods/mood_detail.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% extends "base_list.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{object.title}}{% endblock %}
|
||||||
|
|
||||||
|
{% block lists %}
|
||||||
|
<div class="row webpage">
|
||||||
|
<div class="webpage-metadata">
|
||||||
|
<p>{{object.description}}</p>
|
||||||
|
</div>
|
||||||
|
{% if object.extract %}
|
||||||
|
<div class="webpage-body" id="article">
|
||||||
|
{{object.extract|linebreaks}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<h3>Last scrobbles</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for scrobble in object.scrobble_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{scrobble.timestamp}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
42
vrobbler/templates/moods/mood_list.html
Normal file
42
vrobbler/templates/moods/mood_list.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{% extends "base_list.html" %}
|
||||||
|
|
||||||
|
{% block title %}Moods{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<style>
|
||||||
|
dl { width: 210px; float:left; margin-right: 10px; }
|
||||||
|
dt a { color:white; text-decoration: none; font-size:smaller; }
|
||||||
|
img { height:200px; width: 200px; object-fit: cover; }
|
||||||
|
dd .right { float:right; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block lists %}
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Title</th>
|
||||||
|
<th scope="col">Scrobbles</th>
|
||||||
|
<th scope="col">Start</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for mood in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{mood.get_absolute_url}}">{{mood}}</a></td>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<td>{{mood.scrobble_count}}</td>
|
||||||
|
<td><a type="button" class="btn btn-sm btn-primary" href="{{mood.get_start_url}}">Scrobble</a></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user