stats.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import calendar
  2. import logging
  3. from datetime import datetime, timedelta
  4. from typing import Optional
  5. import pytz
  6. from django.apps import apps
  7. from django.conf import settings
  8. from django.contrib.auth import get_user_model
  9. from django.db.models import Count, Q
  10. from django.utils import timezone
  11. User = get_user_model()
  12. logger = logging.getLogger(__name__)
  13. def get_start_end_dates_by_week(year, week, tz):
  14. d = datetime(year, 1, 1, tzinfo=tz)
  15. if d.weekday() <= 3:
  16. d = d - timedelta(d.weekday())
  17. else:
  18. d = d + timedelta(7 - d.weekday())
  19. dlt = timedelta(days=(week - 1) * 7)
  20. return d + dlt, d + dlt + timedelta(days=6)
  21. def get_scrobble_count_qs(
  22. year: Optional[int] = None,
  23. month: Optional[int] = None,
  24. week: Optional[int] = None,
  25. day: Optional[int] = None,
  26. user=None,
  27. model_str="Track",
  28. ) -> dict[str, int]:
  29. tz = settings.TIME_ZONE
  30. if user and user.is_authenticated:
  31. tz = pytz.timezone(user.profile.timezone)
  32. tz = pytz.utc
  33. data_model = apps.get_model(app_label='music', model_name='Track')
  34. if model_str == "Artist":
  35. data_model = apps.get_model(app_label='music', model_name='Artist')
  36. if model_str == "Video":
  37. data_model = apps.get_model(app_label='videos', model_name='Video')
  38. if model_str == "SportEvent":
  39. data_model = apps.get_model(
  40. app_label='sports', model_name='SportEvent'
  41. )
  42. if model_str == "Artist":
  43. base_qs = data_model.objects.filter(
  44. track__scrobble__user=user,
  45. track__scrobble__played_to_completion=True,
  46. )
  47. else:
  48. base_qs = data_model.objects.filter(
  49. scrobble__user=user,
  50. scrobble__played_to_completion=True,
  51. )
  52. # Returna all media items with scrobble count annotated
  53. if not year:
  54. return base_qs.annotate(scrobble_count=Count("scrobble")).order_by(
  55. "-scrobble_count"
  56. )
  57. start = datetime(year, 1, 1, tzinfo=tz)
  58. end = datetime(year, 12, 31, tzinfo=tz)
  59. if year and day and month:
  60. logger.debug('Filtering by year, month and day')
  61. start = datetime(year, month, day, 0, 0, tzinfo=tz)
  62. end = datetime(year, month, day, 23, 59, tzinfo=tz)
  63. elif year and week:
  64. logger.debug('Filtering by year and week')
  65. start, end = get_start_end_dates_by_week(year, week, tz)
  66. elif month:
  67. logger.debug('Filtering by month')
  68. end_day = calendar.monthrange(year, month)[1]
  69. start = datetime(year, month, 1, tzinfo=tz)
  70. end = datetime(year, month, end_day, tzinfo=tz)
  71. if model_str == "Artist":
  72. scrobble_date_filter = Q(
  73. track__scrobble__timestamp__gte=start,
  74. track__scrobble__timestamp__lte=end,
  75. )
  76. qs = (
  77. base_qs.filter(scrobble_date_filter)
  78. .annotate(scrobble_count=Count("track__scrobble", distinct=True))
  79. .order_by("-scrobble_count")
  80. )
  81. else:
  82. scrobble_date_filter = Q(
  83. scrobble__timestamp__gte=start, scrobble__timestamp__lte=end
  84. )
  85. qs = (
  86. base_qs.filter(scrobble_date_filter)
  87. .annotate(scrobble_count=Count("scrobble", distinct=True))
  88. .order_by("-scrobble_count")
  89. )
  90. return qs
  91. def build_charts(
  92. user: "User",
  93. year: Optional[int] = None,
  94. month: Optional[int] = None,
  95. week: Optional[int] = None,
  96. day: Optional[int] = None,
  97. model_str="Track",
  98. ):
  99. ChartRecord = apps.get_model(
  100. app_label='scrobbles', model_name='ChartRecord'
  101. )
  102. results = get_scrobble_count_qs(year, month, week, day, user, model_str)
  103. unique_counts = list(set([result.scrobble_count for result in results]))
  104. unique_counts.sort(reverse=True)
  105. ranks = {}
  106. for rank, count in enumerate(unique_counts, start=1):
  107. ranks[count] = rank
  108. chart_records = []
  109. for result in results:
  110. chart_record = {
  111. 'year': year,
  112. 'week': week,
  113. 'month': month,
  114. 'day': day,
  115. 'user': user,
  116. }
  117. chart_record['rank'] = ranks[result.scrobble_count]
  118. chart_record['count'] = result.scrobble_count
  119. if model_str == 'Track':
  120. chart_record['track'] = result
  121. if model_str == 'Video':
  122. chart_record['video'] = result
  123. if model_str == 'Artist':
  124. chart_record['artist'] = result
  125. chart_records.append(ChartRecord(**chart_record))
  126. ChartRecord.objects.bulk_create(
  127. chart_records, ignore_conflicts=True, batch_size=500
  128. )
  129. def build_yesterdays_charts_for_user(user: "User", model_str="Track") -> None:
  130. """Given a user calculate needed charts."""
  131. ChartRecord = apps.get_model(
  132. app_label='scrobbles', model_name='ChartRecord'
  133. )
  134. tz = pytz.timezone(settings.TIME_ZONE)
  135. if user and user.is_authenticated:
  136. tz = pytz.timezone(user.profile.timezone)
  137. now = timezone.now().astimezone(tz)
  138. yesterday = now - timedelta(days=1)
  139. logger.info(
  140. f"Generating charts for yesterday ({yesterday.date()}) for {user}"
  141. )
  142. # Always build yesterday's chart
  143. ChartRecord.build(
  144. user,
  145. year=yesterday.year,
  146. month=yesterday.month,
  147. day=yesterday.day,
  148. model_str=model_str,
  149. )
  150. now_week = now.isocalendar()[1]
  151. yesterday_week = now.isocalendar()[1]
  152. if now_week != yesterday_week:
  153. logger.info(
  154. f"New weekly charts for {yesterday.year}-{yesterday_week} for {user}"
  155. )
  156. ChartRecord.build(
  157. user,
  158. year=yesterday.year,
  159. month=yesterday_week,
  160. model_str=model_str,
  161. )
  162. # If the month has changed, build charts
  163. if now.month != yesterday.month:
  164. logger.info(
  165. f"New monthly charts for {yesterday.year}-{yesterday.month} for {user}"
  166. )
  167. ChartRecord.build(
  168. user,
  169. year=yesterday.year,
  170. month=yesterday.month,
  171. model_str=model_str,
  172. )
  173. # If the year has changed, build charts
  174. if now.year != yesterday.year:
  175. logger.info(f"New annual charts for {yesterday.year} for {user}")
  176. ChartRecord.build(user, year=yesterday.year, model_str=model_str)
  177. def build_missing_charts_for_user(user: "User", model_str="Track") -> None:
  178. """"""
  179. ChartRecord = apps.get_model(
  180. app_label='scrobbles', model_name='ChartRecord'
  181. )
  182. Scrobble = apps.get_model(app_label='scrobbles', model_name='Scrobble')
  183. logger.info(f"Generating historical charts for {user}")
  184. tz = pytz.timezone(settings.TIME_ZONE)
  185. if user and user.is_authenticated:
  186. tz = pytz.timezone(user.profile.timezone)
  187. now = timezone.now().astimezone(tz)
  188. first_scrobble = (
  189. Scrobble.objects.filter(user=user, played_to_completion=True)
  190. .order_by('created')
  191. .first()
  192. )
  193. start_date = first_scrobble.timestamp
  194. days_since = (now - start_date).days
  195. for day_num in range(0, days_since):
  196. build_date = start_date + timedelta(days=day_num)
  197. logger.info(f"Generating chart batch for {build_date}")
  198. ChartRecord.build(user=user, year=build_date.year)
  199. ChartRecord.build(
  200. user=user, year=build_date.year, week=build_date.isocalendar()[1]
  201. )
  202. ChartRecord.build(
  203. user=user, year=build_date.year, month=build_date.month
  204. )
  205. ChartRecord.build(
  206. user=user,
  207. year=build_date.year,
  208. month=build_date.month,
  209. day=build_date.day,
  210. )