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,