sessions / f7d99f8b55561c863fc020edd8288459f6eecace

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

ignore whitespace

commit f7d99f8b55561c863fc020edd8288459f6eecaceAuthor: Colin Powell <colin@unbl.ink>Date:   Mon Jul 13 23:17:14 2026 -0400    [drinks] Add water drink notificationsdiff --git a/PROJECT.org b/PROJECT.orgindex 687cac5..717d682 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [1/25] :vrobbler:project:personal:+* Backlog [2/26] :vrobbler:project:personal: ** TODO [#C] After transition to linux add curl_cffi as webpage scrapper again :webpages:metadata: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles: :PROPERTIES:@@ -619,6 +619,10 @@ The Edit log form should have from top to bottom:   - Expansion ids (which should a multi-select widget of expansions for this game)   - Location (which should be a drop down of BoardGameLocations for this user) +** DONE [#B] Send water drinking notifications :drinks:notifications:+:PROPERTIES:+:ID:       6c0db717-a0c7-34a1-57e1-941db0188875+:END: ** DONE [#B] Add wine as a ScrobbleItem in a drinks app and move beers into drinks app :drinks:beer: :PROPERTIES: :ID:       add1be0d-eb33-3273-37bf-0f2cab7e67c5diff --git a/vrobbler/apps/profiles/forms.py b/vrobbler/apps/profiles/forms.pyindex f3c8bc4..448a54e 100644--- a/vrobbler/apps/profiles/forms.py+++ b/vrobbler/apps/profiles/forms.py@@ -42,6 +42,9 @@ class UserProfileForm(forms.ModelForm):             "live_now_playing",             "weigh_in_units",             "volume_unit",+            "default_water_format",+            "default_water_size",+            "water_reminder_enabled",             "trends_disabled",         ]         widgets = {diff --git a/vrobbler/apps/profiles/migrations/0042_add_water_defaults.py b/vrobbler/apps/profiles/migrations/0042_add_water_defaults.pynew file mode 100644index 0000000..0db7a81--- /dev/null+++ b/vrobbler/apps/profiles/migrations/0042_add_water_defaults.py@@ -0,0 +1,39 @@+# Generated by Django 4.2.29 on 2026-07-14 00:53++from django.db import migrations, models+++class Migration(migrations.Migration):++    dependencies = [+        ("profiles", "0041_userprofile_volume_unit"),+    ]++    operations = [+        migrations.AddField(+            model_name="userprofile",+            name="default_water_format",+            field=models.CharField(+                choices=[+                    ("aluminum_can", "Aluminum Can"),+                    ("glass_bottle", "Glass Bottle"),+                    ("plastic_bottle", "Plastic Bottle"),+                    ("metal_bottle", "Metal Bottle"),+                    ("keg", "Keg"),+                    ("by_the_glass", "By the Glass"),+                    ("draft", "Draft"),+                    ("growler", "Growler"),+                    ("box", "Box"),+                ],+                default="by_the_glass",+                max_length=20,+            ),+        ),+        migrations.AddField(+            model_name="userprofile",+            name="default_water_size",+            field=models.PositiveIntegerField(+                default=250, help_text="Default water scrobble size in mL"+            ),+        ),+    ]diff --git a/vrobbler/apps/profiles/migrations/0043_add_water_reminder.py b/vrobbler/apps/profiles/migrations/0043_add_water_reminder.pynew file mode 100644index 0000000..ff43610--- /dev/null+++ b/vrobbler/apps/profiles/migrations/0043_add_water_reminder.py@@ -0,0 +1,18 @@+# Generated by Django 4.2.29 on 2026-07-14 03:12++from django.db import migrations, models+++class Migration(migrations.Migration):++    dependencies = [+        ("profiles", "0042_add_water_defaults"),+    ]++    operations = [+        migrations.AddField(+            model_name="userprofile",+            name="water_reminder_enabled",+            field=models.BooleanField(default=False),+        ),+    ]diff --git a/vrobbler/apps/profiles/models.py b/vrobbler/apps/profiles/models.pyindex 21c1c85..1ae9b69 100644--- a/vrobbler/apps/profiles/models.py+++ b/vrobbler/apps/profiles/models.py@@ -33,6 +33,18 @@ class VolumeUnit(models.TextChoices):     IMPERIAL = "imperial", "Imperial (oz)"  +class WaterFormat(models.TextChoices):+    ALUMINUM_CAN = "aluminum_can", "Aluminum Can"+    GLASS_BOTTLE = "glass_bottle", "Glass Bottle"+    PLASTIC_BOTTLE = "plastic_bottle", "Plastic Bottle"+    METAL_BOTTLE = "metal_bottle", "Metal Bottle"+    KEG = "keg", "Keg"+    BY_THE_GLASS = "by_the_glass", "By the Glass"+    DRAFT = "draft", "Draft"+    GROWLER = "growler", "Growler"+    BOX = "box", "Box"++ class UserProfile(TimeStampedModel):     user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")     timezone = models.CharField(@@ -121,6 +133,19 @@ class UserProfile(TimeStampedModel):         default=VolumeUnit.METRIC,     ) +    default_water_format = models.CharField(+        max_length=20,+        choices=WaterFormat.choices,+        default=WaterFormat.BY_THE_GLASS,+    )++    default_water_size = models.PositiveIntegerField(+        default=250,+        help_text="Default water scrobble size in mL",+    )++    water_reminder_enabled = models.BooleanField(default=False)+     trends_disabled = models.BooleanField(default=False)      disabled_trends = models.JSONField(diff --git a/vrobbler/apps/scrobbles/management/commands/send_water_reminders.py b/vrobbler/apps/scrobbles/management/commands/send_water_reminders.pynew file mode 100644index 0000000..517df3c--- /dev/null+++ b/vrobbler/apps/scrobbles/management/commands/send_water_reminders.py@@ -0,0 +1,9 @@+from django.core.management.base import BaseCommand++from vrobbler.apps.scrobbles.utils import send_water_reminder_notifications+++class Command(BaseCommand):+    def handle(self, *args, **options):+        sent_count = send_water_reminder_notifications()+        print(f"Sent {sent_count} water reminder notifications")diff --git a/vrobbler/apps/scrobbles/notifications.py b/vrobbler/apps/scrobbles/notifications.pyindex 5aa3d70..2270864 100644--- a/vrobbler/apps/scrobbles/notifications.py+++ b/vrobbler/apps/scrobbles/notifications.py@@ -1,6 +1,6 @@ from abc import ABC, abstractmethod-import requests +import requests from django.conf import settings from django.contrib.sites.models import Site from django.urls import reverse@@ -119,3 +119,26 @@ class MoodNtfyNotification(BasicNtfyNotification):                     "Click": self.click_url,                 },             )+++class WaterReminderNtfyNotification(BasicNtfyNotification):+    def __init__(self, profile, **kwargs):+        super().__init__(profile)+        self.ntfy_str: str = "Time to drink some water!"+        self.click_url = self.url_tmpl.format(+            path=reverse("drinks:quick_water_scrobble")+        )+        self.title = "Water Reminder"++    def send(self):+        if self.profile and self.profile.ntfy_enabled and self.profile.ntfy_url:+            requests.post(+                self.profile.ntfy_url,+                data=self.ntfy_str.encode(encoding="utf-8"),+                headers={+                    "Title": self.title,+                    "Priority": "high",+                    "Tags": "droplet, check",+                    "Click": self.click_url,+                },+            )diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.pyindex a501015..506a78c 100644--- a/vrobbler/apps/scrobbles/scrobblers.py+++ b/vrobbler/apps/scrobbles/scrobblers.py@@ -15,7 +15,7 @@ from bricksets.models import BrickSet from dateutil.parser import parse from discgolf.models import DiscGolfCourse from django.utils import timezone-from drinks.models import Beer, Coffee, Wine, Drink, DrinkFormat+from drinks.models import Beer, Coffee, Drink, DrinkFormat, Wine from foods.models import Food from foods.sources.rscraper import RecipeScraperService from locations.constants import LOCATION_PROVIDERS@@ -1361,12 +1361,20 @@ def manual_scrobble_water(         logger.error("Could not find or create Water instance")         return +    water_format = DrinkFormat.BY_THE_GLASS+    water_size_ml = 250++    profile = UserProfile.objects.filter(user_id=user_id).first()+    if profile:+        water_format = profile.default_water_format or water_format+        water_size_ml = profile.default_water_size or water_size_ml+     scrobble_dict = {         "user_id": user_id,         "timestamp": timezone.now(),         "playback_position_seconds": 0,         "source": source,-        "log": {"format": DrinkFormat.BY_THE_GLASS, "size_ml": 250},+        "log": {"format": water_format, "size_ml": water_size_ml},     }     logger.info(         "[vrobbler-scrobble] water scrobble request received",diff --git a/vrobbler/apps/scrobbles/tasks.py b/vrobbler/apps/scrobbles/tasks.pyindex 6736050..07cc407 100644--- a/vrobbler/apps/scrobbles/tasks.py+++ b/vrobbler/apps/scrobbles/tasks.py@@ -276,15 +276,11 @@ def push_scrobble_to_archivebox(scrobble_id):      scrobble = Scrobble.objects.filter(id=scrobble_id).first()     if not scrobble:-        logger.warning(-            "Scrobble %s not found for archivebox push", scrobble_id-        )+        logger.warning("Scrobble %s not found for archivebox push", scrobble_id)         return     webpage = scrobble.web_page     if not webpage:-        logger.warning(-            "Scrobble %s has no web_page for archivebox push", scrobble_id-        )+        logger.warning("Scrobble %s has no web_page for archivebox push", scrobble_id)         return     webpage.push_to_archivebox(scrobble.user) @@ -582,6 +578,14 @@ def send_mood_checkin():     send_mood_checkin_reminders()  +@shared_task+def send_water_reminder():+    """Send water reminder notifications during daylight hours."""+    from vrobbler.apps.scrobbles.utils import send_water_reminder_notifications++    send_water_reminder_notifications()++ @shared_task def backfill_scrobble_sentiment():     """Backfill VADER sentiment for scrobbles with notes (replaces @hourly cron)."""diff --git a/vrobbler/apps/scrobbles/utils.py b/vrobbler/apps/scrobbles/utils.pyindex 63dba69..02a1bbb 100644--- a/vrobbler/apps/scrobbles/utils.py+++ b/vrobbler/apps/scrobbles/utils.py@@ -1,7 +1,6 @@ import hashlib import html import logging-import requests import re from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Optional@@ -10,6 +9,7 @@ from zoneinfo import ZoneInfo  import pendulum import pytz+import requests from django.apps import apps from django.contrib.auth import get_user_model from django.db import models@@ -23,6 +23,7 @@ from scrobbles.constants import LONG_PLAY_MEDIA from scrobbles.notifications import (     MoodNtfyNotification,     ScrobbleNtfyNotification,+    WaterReminderNtfyNotification, ) from scrobbles.tasks import (     process_koreader_import,@@ -154,8 +155,7 @@ def import_lastfm_for_all_users(restart=False):             last_processed = lfm_import.processed_finished         else:             logger.info(-                "No existing LastFM import for user %s, "-                "starting a full parse",+                "No existing LastFM import for user %s, " "starting a full parse",                 user_id,             )             last_processed = None@@ -350,6 +350,20 @@ def send_mood_checkin_reminders() -> int:     return notifications_sent  +def send_water_reminder_notifications() -> int:+    """Send water reminders to users with the setting enabled, during daylight hours.++    Only sends if the user's local time is between 6 AM and 11 PM."""+    notifications_sent = 0+    for profile in UserProfile.objects.filter(water_reminder_enabled=True):+        local_hour = timezone.localtime(timezone.now(), profile.tzinfo).hour+        if 6 <= local_hour <= 22:+            WaterReminderNtfyNotification(profile).send()+            notifications_sent += 1++    return notifications_sent++ def extract_domain(url):     parsed_url = urlparse(url)     domain = parsed_url.netloc.split(".")[-2] + "." + parsed_url.netloc.split(".")[-1]@@ -460,7 +474,8 @@ def _ensure_mopidy_playlist(profile):         pass      result = _mopidy_rpc(-        profile, "core.playlists.create",+        profile,+        "core.playlists.create",         {"name": playlist_name, "uri_scheme": "m3u"},     )     logger.info(@@ -588,7 +603,8 @@ def remove_track_from_mopidy_favorites_playlist(favorite):         if playlist and playlist.get("uri"):             existing_tracks = playlist.get("tracks") or []             filtered = [-                t for t in existing_tracks+                t+                for t in existing_tracks                 if not (isinstance(t, dict) and t.get("uri") == mopidy_uri)             ]             if len(filtered) == len(existing_tracks):@@ -639,7 +655,8 @@ def _ensure_mopidy_playlist_by_name(profile, playlist_name):         pass      result = _mopidy_rpc(-        profile, "core.playlists.create",+        profile,+        "core.playlists.create",         {"name": playlist_name, "uri_scheme": "m3u"},     )     return resultdiff --git a/vrobbler/settings.py b/vrobbler/settings.pyindex 5324314..6ef61b9 100644--- a/vrobbler/settings.py+++ b/vrobbler/settings.py@@ -188,6 +188,10 @@ CELERY_BEAT_SCHEDULE = {         "task": "scrobbles.tasks.send_mood_checkin",         "schedule": crontab(hour="*/4", minute=0),     },+    "send-water-reminder": {+        "task": "scrobbles.tasks.send_water_reminder",+        "schedule": crontab(hour="*/3", minute=0),+    },     "backfill-scrobble-sentiment": {         "task": "scrobbles.tasks.backfill_scrobble_sentiment",         "schedule": crontab(minute="0"),diff --git a/vrobbler/templates/base.html b/vrobbler/templates/base.htmlindex 4978d65..b0c183f 100644--- a/vrobbler/templates/base.html+++ b/vrobbler/templates/base.html@@ -281,10 +281,7 @@                     <a class="nav-link px-3" href="{% url "moods:checkin" %}">Check-in</a>                 </div>                 <div class="nav-item text-nowrap">-                    <form method="post" action="{% url "drinks:quick_water_scrobble" %}" style="display:inline;">-                        {% csrf_token %}-                        <button type="submit" class="nav-link px-3 btn btn-link" style="text-decoration:none;">Drink water</button>-                    </form>+                    <a class="nav-link px-3" href="{% url "drinks:quick_water_scrobble" %}">Drink water</a>                 </div>                 <div class="nav-item text-nowrap">                     <a class="nav-link px-3" href="{% url "profiles:profile_settings" %}">Settings</a>