diff --git a/PROJECT.org b/PROJECT.org index 54e1be5..8dd67fc 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [0/23] :vrobbler:project:personal: +* Backlog [1/24] :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: @@ -600,6 +600,10 @@ The Edit log form should have from top to bottom: *** Description +** DONE Fix bug in trends and add liquid fasting :food:fasting:trends: +:PROPERTIES: +:ID: 4736676e-95b0-cb4c-0eaa-c151f3753cd0 +:END: * Version 61.1 [1/1] ** DONE [#B] Add trend for fasting :food:fasting:trends: :PROPERTIES: diff --git a/vrobbler/apps/trends/templates/trends/_fasting.html b/vrobbler/apps/trends/templates/trends/_fasting.html index 6ab2116..48f2ee6 100644 --- a/vrobbler/apps/trends/templates/trends/_fasting.html +++ b/vrobbler/apps/trends/templates/trends/_fasting.html @@ -44,7 +44,8 @@

Periodic fasting: {{ data.periodic_threshold_hours }}h+ gaps ({{ data.periodic_fasting_count }} days) · - Full fasting: {{ data.full_threshold_hours }}h+ gaps ({{ data.full_fasting_count }} days) + Full fasting: {{ data.full_threshold_hours }}h+ gaps ({{ data.full_fasting_count }} days) · + Liquid fasting: {{ data.liquid_fasting_count }} days

@@ -117,9 +118,13 @@ {{ day.date }} - - {{ day.type|title }} - + {% if day.type == 'full' and day.had_drink %} + Liquid Fast + {% elif day.type == 'full' %} + Full Fast + {% else %} + Periodic Fast + {% endif %} {% endfor %} diff --git a/vrobbler/apps/trends/trends/fasting.py b/vrobbler/apps/trends/trends/fasting.py index 0f34afb..43a936f 100644 --- a/vrobbler/apps/trends/trends/fasting.py +++ b/vrobbler/apps/trends/trends/fasting.py @@ -1,4 +1,3 @@ -from collections import defaultdict from datetime import timedelta from django.db.models import Q @@ -6,6 +5,14 @@ from django.utils import timezone from scrobbles.models import Scrobble +DRINK_MEDIA_TYPES = [ + Scrobble.MediaType.DRINK, + Scrobble.MediaType.BEER, + Scrobble.MediaType.WINE, + Scrobble.MediaType.COFFEE, +] + + def _food_scrobbles(user, start, end): filters = Q(user=user, media_type=Scrobble.MediaType.FOOD) if start: @@ -15,8 +22,16 @@ def _food_scrobbles(user, start, end): return Scrobble.objects.filter(filters).order_by("timestamp") +def _drinks_in_window(user, since, until): + filters = Q(user=user, media_type__in=DRINK_MEDIA_TYPES) + if since: + filters &= Q(timestamp__gte=since) + if until: + filters &= Q(timestamp__lte=until) + return Scrobble.objects.filter(filters).exists() + + def _vacation_ranges(user, start, end): - """Return list of (start, end) tuples for Vacation life events.""" filters = Q( user=user, media_type=Scrobble.MediaType.LIFE_EVENT, @@ -41,6 +56,14 @@ def _is_vacation_day(date, vacation_ranges): return False +def _classify_gap(gap_hours, periodic_hours, full_hours): + if gap_hours >= full_hours: + return "full" + if gap_hours >= periodic_hours: + return "periodic" + return "normal" + + def compute_fasting(user, period="last_30"): from trends.utils import get_date_range @@ -63,6 +86,7 @@ def compute_fasting(user, period="last_30"): "fasting_days_count": 0, "periodic_fasting_count": 0, "full_fasting_count": 0, + "liquid_fasting_count": 0, "fasting_rate_pct": 0.0, "fasting_days": [], "current_streak": {"count": 0, "type": None}, @@ -71,62 +95,85 @@ def compute_fasting(user, period="last_30"): "full_threshold_hours": full_hours, } + food_times = [s.timestamp for s in food] + food_dates = {s.timestamp.date() for s in food} + + now = timezone.now() + period_start = start.date() if start else food_times[0].date() + period_end = end.date() if end else now.date() + fasting_by_date = {} - fasting_days_list = [] + day = period_start - for i in range(len(food) - 1): - t1 = food[i].timestamp - t2 = food[i + 1].timestamp - gap = t2 - t1 - gap_hours = gap.total_seconds() / 3600 - - if gap_hours < periodic_hours: + while day <= period_end: + if day in food_dates: + day += timedelta(days=1) continue - if gap_hours >= full_hours: - ftype = "full" - else: - ftype = "periodic" - - day = t1.date() + timedelta(days=1) - end_day = t2.date() - - while day < end_day: - if vacation_exempt and _is_vacation_day(day, vacation_ranges): - day += timedelta(days=1) - continue - - existing = fasting_by_date.get(day) - if existing is None or (ftype == "full" and existing["type"] == "periodic"): - fasting_by_date[day] = {"date": str(day), "type": ftype} + if vacation_exempt and _is_vacation_day(day, vacation_ranges): day += timedelta(days=1) + continue + + prev_time = None + for t in reversed(food_times): + if t.date() < day: + prev_time = t + break + + if day == now.date(): + next_time = now + else: + next_time = None + for t in food_times: + if t.date() > day: + next_time = t + break + + fasting_type = None + if prev_time and next_time: + gap_hours = (next_time - prev_time).total_seconds() / 3600 + fasting_type = _classify_gap(gap_hours, periodic_hours, full_hours) + elif prev_time: + gap_hours = (now - prev_time).total_seconds() / 3600 + fasting_type = _classify_gap(gap_hours, periodic_hours, full_hours) + + if fasting_type is None: + day += timedelta(days=1) + continue + + entry = {"date": str(day), "type": fasting_type, "had_drink": False} + + if fasting_type == "full" and prev_time and next_time: + entry["had_drink"] = _drinks_in_window(user, prev_time, next_time) + + existing = fasting_by_date.get(day) + if existing is None or ( + fasting_type == "full" and existing["type"] == "periodic" + ): + fasting_by_date[day] = entry + + day += timedelta(days=1) fasting_days_list = sorted(fasting_by_date.values(), key=lambda x: x["date"]) - now = timezone.now().date() - if start: - period_start = start.date() - else: - period_start = food[0].timestamp.date() if food else now - if end: - period_end = end.date() - else: - period_end = now - total_days = (period_end - period_start).days + 1 fasting_count = len(fasting_days_list) periodic_count = sum(1 for d in fasting_days_list if d["type"] == "periodic") full_count = sum(1 for d in fasting_days_list if d["type"] == "full") + liquid_count = sum( + 1 for d in fasting_days_list if d["type"] == "full" and d["had_drink"] + ) rate = round((fasting_count / total_days) * 100, 1) if total_days > 0 else 0.0 longest_streaks = _compute_streaks(fasting_days_list) - current_streak = _compute_current_streak(fasting_days_list, now) + current_streak = _compute_current_streak(fasting_days_list, now.date()) return { "total_days": total_days, "fasting_days_count": fasting_count, "periodic_fasting_count": periodic_count, "full_fasting_count": full_count, + "liquid_fasting_count": liquid_count, "fasting_rate_pct": rate, "fasting_days": fasting_days_list, "current_streak": current_streak,