views.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. import calendar
  2. import json
  3. import logging
  4. from datetime import datetime, timedelta
  5. import pytz
  6. from django.apps import apps
  7. from django.conf import settings
  8. from django.contrib import messages
  9. from django.contrib.auth.mixins import LoginRequiredMixin
  10. from django.db.models import Count, Q
  11. from django.db.models.query import QuerySet
  12. from django.http import FileResponse, HttpResponseRedirect, JsonResponse
  13. from django.urls import reverse_lazy
  14. from django.utils import timezone
  15. from django.views.decorators.csrf import csrf_exempt
  16. from django.views.generic import DetailView, FormView, TemplateView
  17. from django.views.generic.edit import CreateView
  18. from django.views.generic.list import ListView
  19. from music.aggregators import live_charts, scrobble_counts, week_of_scrobbles
  20. from rest_framework import status
  21. from rest_framework.decorators import (
  22. api_view,
  23. parser_classes,
  24. permission_classes,
  25. )
  26. from rest_framework.parsers import MultiPartParser
  27. from rest_framework.permissions import IsAuthenticated
  28. from rest_framework.response import Response
  29. from scrobbles.api import serializers
  30. from scrobbles.constants import (
  31. LONG_PLAY_MEDIA,
  32. MANUAL_SCROBBLE_FNS,
  33. PLAY_AGAIN_MEDIA,
  34. SCROBBLE_CONTENT_URLS,
  35. )
  36. from scrobbles.export import export_scrobbles
  37. from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
  38. from scrobbles.models import (
  39. AudioScrobblerTSVImport,
  40. ChartRecord,
  41. KoReaderImport,
  42. LastFmImport,
  43. RetroarchImport,
  44. Scrobble,
  45. )
  46. from scrobbles.scrobblers import *
  47. from scrobbles.tasks import (
  48. process_koreader_import,
  49. process_lastfm_import,
  50. process_tsv_import,
  51. )
  52. from scrobbles.utils import (
  53. get_long_plays_completed,
  54. get_long_plays_in_progress,
  55. get_recently_played_board_games,
  56. )
  57. logger = logging.getLogger(__name__)
  58. class ScrobbleableListView(ListView):
  59. model = None
  60. paginate_by = 200
  61. def get_queryset(self):
  62. queryset = super().get_queryset()
  63. if not self.request.user.is_anonymous:
  64. queryset = queryset.annotate(
  65. scrobble_count=Count("scrobble"),
  66. filter=Q(scrobble__user=self.request.user),
  67. ).order_by("-scrobble_count")
  68. else:
  69. queryset = queryset.annotate(
  70. scrobble_count=Count("scrobble")
  71. ).order_by("-scrobble_count")
  72. return queryset
  73. class ScrobbleableDetailView(DetailView):
  74. model = None
  75. slug_field = "uuid"
  76. def get_context_data(self, **kwargs):
  77. context_data = super().get_context_data(**kwargs)
  78. context_data["scrobbles"] = list()
  79. if not self.request.user.is_anonymous:
  80. context_data["scrobbles"] = self.object.scrobble_set.filter(
  81. user=self.request.user
  82. )
  83. return context_data
  84. class RecentScrobbleList(ListView):
  85. model = Scrobble
  86. def get(self, *args, **kwargs):
  87. user = self.request.user
  88. if user.is_authenticated:
  89. if scrobble_url := self.request.GET.get("scrobble_url", ""):
  90. scrobble = manual_scrobble_from_url(
  91. scrobble_url, self.request.user.id
  92. )
  93. return HttpResponseRedirect(scrobble.redirect_url(user.id))
  94. return super().get(*args, **kwargs)
  95. def get_context_data(self, **kwargs):
  96. data = super().get_context_data(**kwargs)
  97. user = self.request.user
  98. if user.is_authenticated:
  99. completed_for_user = Scrobble.objects.filter(
  100. played_to_completion=True, user=user
  101. )
  102. data["long_play_in_progress"] = get_long_plays_in_progress(user)
  103. data["play_again"] = get_recently_played_board_games(user)
  104. data["video_scrobble_list"] = completed_for_user.filter(
  105. video__isnull=False
  106. ).order_by("-timestamp")[:15]
  107. data["podcast_scrobble_list"] = completed_for_user.filter(
  108. podcast_episode__isnull=False
  109. ).order_by("-timestamp")[:15]
  110. data["sport_scrobble_list"] = completed_for_user.filter(
  111. sport_event__isnull=False
  112. ).order_by("-timestamp")[:15]
  113. data["videogame_scrobble_list"] = completed_for_user.filter(
  114. video_game__isnull=False
  115. ).order_by("-timestamp")[:15]
  116. data["boardgame_scrobble_list"] = completed_for_user.filter(
  117. board_game__isnull=False
  118. ).order_by("-timestamp")[:15]
  119. data["active_imports"] = AudioScrobblerTSVImport.objects.filter(
  120. processing_started__isnull=False,
  121. processed_finished__isnull=True,
  122. user=self.request.user,
  123. )
  124. data["counts"] = scrobble_counts(user)
  125. else:
  126. data["weekly_data"] = week_of_scrobbles()
  127. data["counts"] = scrobble_counts()
  128. data["imdb_form"] = ScrobbleForm
  129. data["export_form"] = ExportScrobbleForm
  130. return data
  131. def get_queryset(self):
  132. return Scrobble.objects.filter(
  133. track__isnull=False, in_progress=False
  134. ).order_by("-timestamp")[:15]
  135. class ScrobbleLongPlaysView(TemplateView):
  136. template_name = "scrobbles/long_plays_in_progress.html"
  137. def get_context_data(self, **kwargs):
  138. context_data = super().get_context_data(**kwargs)
  139. context_data["view"] = self.request.GET.get("view", "grid")
  140. context_data["in_progress"] = get_long_plays_in_progress(
  141. self.request.user
  142. )
  143. context_data["completed"] = get_long_plays_completed(self.request.user)
  144. return context_data
  145. class ScrobbleImportListView(TemplateView):
  146. template_name = "scrobbles/import_list.html"
  147. def get_context_data(self, **kwargs):
  148. context_data = super().get_context_data(**kwargs)
  149. context_data["object_list"] = []
  150. context_data["tsv_imports"] = AudioScrobblerTSVImport.objects.filter(
  151. user=self.request.user,
  152. ).order_by("-processing_started")[:10]
  153. context_data["koreader_imports"] = KoReaderImport.objects.filter(
  154. user=self.request.user,
  155. ).order_by("-processing_started")[:10]
  156. context_data["lastfm_imports"] = LastFmImport.objects.filter(
  157. user=self.request.user,
  158. ).order_by("-processing_started")[:10]
  159. context_data["retroarch_imports"] = RetroarchImport.objects.filter(
  160. user=self.request.user,
  161. ).order_by("-processing_started")[:10]
  162. return context_data
  163. class BaseScrobbleImportDetailView(DetailView):
  164. slug_field = "uuid"
  165. template_name = "scrobbles/import_detail.html"
  166. def get_queryset(self):
  167. return super().get_queryset().filter(user=self.request.user)
  168. def get_context_data(self, **kwargs):
  169. context_data = super().get_context_data(**kwargs)
  170. title = "Generic Scrobble Import"
  171. if self.model == KoReaderImport:
  172. title = "KoReader Import"
  173. if self.model == AudioScrobblerTSVImport:
  174. title = "Audioscrobbler TSV Import"
  175. if self.model == LastFmImport:
  176. title = "LastFM Import"
  177. if self.model == RetroarchImport:
  178. title = "Retroarch Import"
  179. context_data["title"] = title
  180. return context_data
  181. class ScrobbleKoReaderImportDetailView(BaseScrobbleImportDetailView):
  182. model = KoReaderImport
  183. class ScrobbleTSVImportDetailView(BaseScrobbleImportDetailView):
  184. model = AudioScrobblerTSVImport
  185. class ScrobbleLastFMImportDetailView(BaseScrobbleImportDetailView):
  186. model = LastFmImport
  187. class ScrobbleRetroarchImportDetailView(BaseScrobbleImportDetailView):
  188. model = RetroarchImport
  189. class ManualScrobbleView(FormView):
  190. form_class = ScrobbleForm
  191. template_name = "scrobbles/manual_form.html"
  192. def form_valid(self, form):
  193. item_str = form.cleaned_data.get("item_id")
  194. logger.debug(f"Looking for scrobblable media with input {item_str}")
  195. key, item_id = item_str[:2], item_str[3:]
  196. scrobble_fn = MANUAL_SCROBBLE_FNS[key]
  197. scrobble = eval(scrobble_fn)(item_id, self.request.user.id)
  198. return HttpResponseRedirect(
  199. scrobble.redirect_url(self.request.user.id)
  200. )
  201. class JsonableResponseMixin:
  202. """
  203. Mixin to add JSON support to a form.
  204. Must be used with an object-based FormView (e.g. CreateView)
  205. """
  206. def form_invalid(self, form):
  207. response = super().form_invalid(form)
  208. if self.request.accepts("text/html"):
  209. return response
  210. else:
  211. return JsonResponse(form.errors, status=400)
  212. def form_valid(self, form):
  213. # We make sure to call the parent's form_valid() method because
  214. # it might do some processing (in the case of CreateView, it will
  215. # call form.save() for example).
  216. response = super().form_valid(form)
  217. if self.request.accepts("text/html"):
  218. return response
  219. else:
  220. data = {
  221. "pk": self.object.pk,
  222. }
  223. return JsonResponse(data)
  224. class AudioScrobblerImportCreateView(
  225. LoginRequiredMixin, JsonableResponseMixin, CreateView
  226. ):
  227. model = AudioScrobblerTSVImport
  228. fields = ["tsv_file"]
  229. template_name = "scrobbles/upload_form.html"
  230. success_url = reverse_lazy("vrobbler-home")
  231. def form_valid(self, form):
  232. self.object = form.save(commit=False)
  233. self.object.user = self.request.user
  234. self.object.save()
  235. process_tsv_import.delay(self.object.id)
  236. return HttpResponseRedirect(self.request.META.get("HTTP_REFERER"))
  237. class KoReaderImportCreateView(
  238. LoginRequiredMixin, JsonableResponseMixin, CreateView
  239. ):
  240. model = KoReaderImport
  241. fields = ["sqlite_file"]
  242. template_name = "scrobbles/upload_form.html"
  243. success_url = reverse_lazy("vrobbler-home")
  244. def form_valid(self, form):
  245. self.object = form.save(commit=False)
  246. self.object.user = self.request.user
  247. self.object.save()
  248. process_koreader_import.delay(self.object.id)
  249. return HttpResponseRedirect(self.request.META.get("HTTP_REFERER"))
  250. @permission_classes([IsAuthenticated])
  251. @api_view(["GET"])
  252. def lastfm_import(request):
  253. lfm_import, created = LastFmImport.objects.get_or_create(
  254. user=request.user, processed_finished__isnull=True
  255. )
  256. process_lastfm_import.delay(lfm_import.id)
  257. return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
  258. @csrf_exempt
  259. @permission_classes([IsAuthenticated])
  260. @api_view(["POST"])
  261. def jellyfin_webhook(request):
  262. post_data = request.data
  263. logger.info(
  264. "[jellyfin_webhook] called",
  265. extra={"post_data": post_data},
  266. )
  267. in_progress = post_data.get("NotificationType", "") == "PlaybackProgress"
  268. is_music = post_data.get("ItemType", "") == "Audio"
  269. # Disregard progress updates
  270. if in_progress and is_music:
  271. logger.info(
  272. "[jellyfin_webhook] ignoring update of music in progress",
  273. extra={"post_data": post_data},
  274. )
  275. return Response({}, status=status.HTTP_304_NOT_MODIFIED)
  276. scrobble = jellyfin_scrobble_media(post_data, request.user.id)
  277. if not scrobble:
  278. return Response({}, status=status.HTTP_400_BAD_REQUEST)
  279. logger.info(
  280. "[jellyfin_webhook] finished",
  281. extra={"scrobble_id": scrobble.id},
  282. )
  283. return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
  284. @csrf_exempt
  285. @permission_classes([IsAuthenticated])
  286. @api_view(["POST"])
  287. def mopidy_webhook(request):
  288. try:
  289. data_dict = json.loads(request.data)
  290. except TypeError:
  291. data_dict = request.data
  292. scrobble = mopidy_scrobble_media(data_dict, request.user.id)
  293. if not scrobble:
  294. return Response({}, status=status.HTTP_400_BAD_REQUEST)
  295. return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
  296. @csrf_exempt
  297. @permission_classes([IsAuthenticated])
  298. @api_view(["POST"])
  299. def gps_webhook(request):
  300. try:
  301. data_dict = json.loads(request.data)
  302. except TypeError:
  303. data_dict = request.data
  304. # For making things easier to build new input processors
  305. if getattr(settings, "DUMP_REQUEST_DATA", False):
  306. json_data = json.dumps(data_dict, indent=4)
  307. logger.debug(f"{json_data}")
  308. # TODO Fix this so we have to authenticate!
  309. user_id = 1
  310. if request.user.id:
  311. user_id = request.user.id
  312. scrobble = gpslogger_scrobble_location(data_dict, user_id)
  313. if not scrobble:
  314. return Response({}, status=status.HTTP_200_OK)
  315. return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
  316. @csrf_exempt
  317. @permission_classes([IsAuthenticated])
  318. @api_view(["POST"])
  319. @parser_classes([MultiPartParser])
  320. def import_audioscrobbler_file(request):
  321. """Takes a TSV file in the Audioscrobbler format, saves it and processes the
  322. scrobbles.
  323. """
  324. scrobbles_created = []
  325. # tsv_file = request.FILES[0]
  326. file_serializer = serializers.AudioScrobblerTSVImportSerializer(
  327. data=request.data
  328. )
  329. if file_serializer.is_valid():
  330. import_file = file_serializer.save()
  331. return Response(
  332. {"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK
  333. )
  334. else:
  335. return Response(
  336. file_serializer.errors, status=status.HTTP_400_BAD_REQUEST
  337. )
  338. @permission_classes([IsAuthenticated])
  339. @api_view(["GET"])
  340. def scrobble_start(request, uuid):
  341. logger.info(
  342. "[scrobble_start] called",
  343. extra={"request": request, "uuid": uuid},
  344. )
  345. user = request.user
  346. success_url = request.META.get("HTTP_REFERER")
  347. if not user.is_authenticated:
  348. return HttpResponseRedirect(success_url)
  349. media_obj = None
  350. for app, model in PLAY_AGAIN_MEDIA.items():
  351. media_model = apps.get_model(app_label=app, model_name=model)
  352. media_obj = media_model.objects.filter(uuid=uuid).first()
  353. if media_obj:
  354. break
  355. if not media_obj:
  356. logger.info(
  357. "[scrobble_start] media object not found",
  358. extra={"uuid": uuid, "user_id": user.id},
  359. )
  360. # TODO Log that we couldn't find a media obj to scrobble
  361. return
  362. scrobble = None
  363. user_id = request.user.id
  364. if media_obj:
  365. media_obj.scrobble_for_user(user_id)
  366. if scrobble:
  367. messages.add_message(
  368. request,
  369. messages.SUCCESS,
  370. f"Scrobble of {scrobble.media_obj} started.",
  371. )
  372. else:
  373. messages.add_message(
  374. request, messages.ERROR, f"Media with uuid {uuid} not found."
  375. )
  376. if (
  377. user.profile.redirect_to_webpage
  378. and media_obj.__class__.__name__ == Scrobble.MediaType.WEBPAGE
  379. ):
  380. logger.info(f"Redirecting to {media_obj} detail apge")
  381. return HttpResponseRedirect(media_obj.url)
  382. return HttpResponseRedirect(success_url)
  383. @api_view(["GET"])
  384. def scrobble_longplay_finish(request, uuid):
  385. user = request.user
  386. success_url = request.META.get("HTTP_REFERER")
  387. if not user.is_authenticated:
  388. return HttpResponseRedirect(success_url)
  389. media_obj = None
  390. for app, model in LONG_PLAY_MEDIA.items():
  391. media_model = apps.get_model(app_label=app, model_name=model)
  392. media_obj = media_model.objects.filter(uuid=uuid).first()
  393. if media_obj:
  394. break
  395. if not media_obj:
  396. return
  397. last_scrobble = media_obj.last_long_play_scrobble_for_user(user)
  398. if last_scrobble and last_scrobble.long_play_complete == False:
  399. last_scrobble.long_play_complete = True
  400. last_scrobble.save(update_fields=["long_play_complete"])
  401. messages.add_message(
  402. request,
  403. messages.SUCCESS,
  404. f"Long play of {media_obj} finished.",
  405. )
  406. else:
  407. messages.add_message(
  408. request, messages.ERROR, f"Media with uuid {uuid} not found."
  409. )
  410. return HttpResponseRedirect(success_url)
  411. @permission_classes([IsAuthenticated])
  412. @api_view(["GET"])
  413. def scrobble_finish(request, uuid):
  414. user = request.user
  415. success_url = request.META.get("HTTP_REFERER")
  416. if not user.is_authenticated:
  417. return HttpResponseRedirect(success_url)
  418. scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
  419. if scrobble:
  420. scrobble.stop(force_finish=True)
  421. messages.add_message(
  422. request,
  423. messages.SUCCESS,
  424. f"Scrobble of {scrobble.media_obj} finished.",
  425. )
  426. else:
  427. messages.add_message(request, messages.ERROR, "Scrobble not found.")
  428. return HttpResponseRedirect(success_url)
  429. @permission_classes([IsAuthenticated])
  430. @api_view(["GET"])
  431. def scrobble_cancel(request, uuid):
  432. user = request.user
  433. success_url = reverse_lazy("vrobbler-home")
  434. if not user.is_authenticated:
  435. return HttpResponseRedirect(success_url)
  436. scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
  437. if scrobble:
  438. scrobble.cancel()
  439. messages.add_message(
  440. request,
  441. messages.SUCCESS,
  442. f"Scrobble of {scrobble.media_obj} cancelled.",
  443. )
  444. else:
  445. messages.add_message(request, messages.ERROR, "Scrobble not found.")
  446. return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
  447. @permission_classes([IsAuthenticated])
  448. @api_view(["GET"])
  449. def export(request):
  450. format = request.GET.get("export_type", "csv")
  451. start = request.GET.get("start")
  452. end = request.GET.get("end")
  453. logger.debug(f"Exporting all scrobbles in format {format}")
  454. temp_file, extension = export_scrobbles(
  455. start_date=start, end_date=end, format=format
  456. )
  457. now = datetime.now()
  458. filename = f"vrobbler-export-{str(now)}.{extension}"
  459. response = FileResponse(open(temp_file, "rb"))
  460. response["Content-Disposition"] = f'attachment; filename="{filename}"'
  461. return response
  462. class ChartRecordView(TemplateView):
  463. template_name = "scrobbles/chart_index.html"
  464. @staticmethod
  465. def get_media_filter(media_type: str = "") -> Q:
  466. filters = {
  467. "Track": Q(track__isnull=False),
  468. "Artist": Q(artist__isnull=False),
  469. "Series": Q(series__isnull=False),
  470. "Video": Q(video__isnull=False),
  471. "": Q(),
  472. }
  473. return filters[media_type]
  474. def get_chart_records(self, media_type: str = "", **kwargs):
  475. media_filter = self.get_media_filter(media_type)
  476. charts = ChartRecord.objects.filter(
  477. media_filter, user=self.request.user, **kwargs
  478. ).order_by("rank")
  479. if charts.count() == 0:
  480. ChartRecord.build(
  481. user=self.request.user, model_str=media_type, **kwargs
  482. )
  483. charts = ChartRecord.objects.filter(
  484. media_filter, user=self.request.user, **kwargs
  485. ).order_by("rank")
  486. return charts
  487. def get_chart(
  488. self, period: str = "all_time", limit=15, media: str = ""
  489. ) -> QuerySet:
  490. now = timezone.now()
  491. params = {}
  492. params["media_type"] = media
  493. if period == "today":
  494. params["day"] = now.day
  495. params["month"] = now.month
  496. params["year"] = now.year
  497. if period == "week":
  498. params["week"] = now.ioscalendar()[1]
  499. params["year"] = now.year
  500. if period == "month":
  501. params["month"] = now.month
  502. params["year"] = now.year
  503. if period == "year":
  504. params["year"] = now.year
  505. return self.get_chart_records(**params)[:limit]
  506. def get_context_data(self, **kwargs):
  507. context_data = super().get_context_data(**kwargs)
  508. date = self.request.GET.get("date")
  509. media_type = self.request.GET.get("media", "Track")
  510. user = self.request.user
  511. params = {}
  512. context_data["artist_charts"] = {}
  513. if not date:
  514. limit = 20
  515. artist_params = {"user": user, "media_type": "Artist"}
  516. context_data["current_artist_charts"] = {
  517. "today": live_charts(
  518. **artist_params, chart_period="today", limit=limit
  519. ),
  520. "week": live_charts(
  521. **artist_params, chart_period="week", limit=limit
  522. ),
  523. "month": live_charts(
  524. **artist_params, chart_period="month", limit=limit
  525. ),
  526. "year": live_charts(
  527. **artist_params, chart_period="year", limit=limit
  528. ),
  529. "all": live_charts(**artist_params, limit=limit),
  530. }
  531. track_params = {"user": user, "media_type": "Track"}
  532. context_data["current_track_charts"] = {
  533. "today": live_charts(
  534. **track_params, chart_period="today", limit=limit
  535. ),
  536. "week": live_charts(
  537. **track_params, chart_period="week", limit=limit
  538. ),
  539. "month": live_charts(
  540. **track_params, chart_period="month", limit=limit
  541. ),
  542. "year": live_charts(
  543. **track_params, chart_period="year", limit=limit
  544. ),
  545. "all": live_charts(**track_params, limit=limit),
  546. }
  547. limit = 14
  548. artist = {"user": user, "media_type": "Artist", "limit": limit}
  549. # This is weird. They don't display properly as QuerySets, so we cast to lists
  550. context_data["chart_keys"] = {
  551. "today": "Today",
  552. "last7": "Last 7 days",
  553. "last30": "Last 30 days",
  554. "year": "This year",
  555. "all": "All time",
  556. }
  557. context_data["current_artist_charts"] = {
  558. "today": list(live_charts(**artist, chart_period="today")),
  559. "last7": list(live_charts(**artist, chart_period="last7")),
  560. "last30": list(live_charts(**artist, chart_period="last30")),
  561. "year": list(live_charts(**artist, chart_period="year")),
  562. "all": list(live_charts(**artist)),
  563. }
  564. track = {"user": user, "media_type": "Track", "limit": limit}
  565. context_data["current_track_charts"] = {
  566. "today": list(live_charts(**track, chart_period="today")),
  567. "last7": list(live_charts(**track, chart_period="last7")),
  568. "last30": list(live_charts(**track, chart_period="last30")),
  569. "year": list(live_charts(**track, chart_period="year")),
  570. "all": list(live_charts(**track)),
  571. }
  572. return context_data
  573. # Date provided, lookup past charts, returning nothing if it's now or in the future.
  574. now = timezone.now()
  575. year = now.year
  576. params = {"year": year}
  577. name = f"Chart for {year}"
  578. date_params = date.split("-")
  579. year = int(date_params[0])
  580. in_progress = False
  581. if len(date_params) == 2:
  582. if "W" in date_params[1]:
  583. week = int(date_params[1].strip('W"'))
  584. params["week"] = week
  585. start = datetime.strptime(date + "-1", "%Y-W%W-%w").replace(
  586. tzinfo=pytz.utc
  587. )
  588. end = start + timedelta(days=6)
  589. in_progress = start <= now <= end
  590. as_str = start.strftime("Week of %B %d, %Y")
  591. name = f"Chart for {as_str}"
  592. else:
  593. month = int(date_params[1])
  594. params["month"] = month
  595. month_str = calendar.month_name[month]
  596. name = f"Chart for {month_str} {year}"
  597. in_progress = now.month == month and now.year == year
  598. if len(date_params) == 3:
  599. month = int(date_params[1])
  600. day = int(date_params[2])
  601. params["month"] = month
  602. params["day"] = day
  603. month_str = calendar.month_name[month]
  604. name = f"Chart for {month_str} {day}, {year}"
  605. in_progress = (
  606. now.month == month and now.year == year and now.day == day
  607. )
  608. media_filter = self.get_media_filter("Track")
  609. track_charts = ChartRecord.objects.filter(
  610. media_filter, user=self.request.user, **params
  611. ).order_by("rank")
  612. media_filter = self.get_media_filter("Artist")
  613. artist_charts = ChartRecord.objects.filter(
  614. media_filter, user=self.request.user, **params
  615. ).order_by("rank")
  616. if track_charts.count() == 0 and not in_progress:
  617. ChartRecord.build(
  618. user=self.request.user, model_str="Track", **params
  619. )
  620. media_filter = self.get_media_filter("Track")
  621. track_charts = ChartRecord.objects.filter(
  622. media_filter, user=self.request.user, **params
  623. ).order_by("rank")
  624. if artist_charts.count() == 0 and not in_progress:
  625. ChartRecord.build(
  626. user=self.request.user, model_str="Artist", **params
  627. )
  628. media_filter = self.get_media_filter("Artist")
  629. artist_charts = ChartRecord.objects.filter(
  630. media_filter, user=self.request.user, **params
  631. ).order_by("rank")
  632. context_data["media_type"] = media_type
  633. context_data["track_charts"] = track_charts
  634. context_data["artist_charts"] = artist_charts
  635. context_data["name"] = " ".join(["Top", media_type, "for", name])
  636. context_data["in_progress"] = in_progress
  637. return context_data
  638. class ScrobbleStatusView(LoginRequiredMixin, TemplateView):
  639. model = Scrobble
  640. template_name = "scrobbles/status.html"
  641. def get_context_data(self, **kwargs):
  642. data = super().get_context_data(**kwargs)
  643. user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp")
  644. progress_plays = user_scrobble_qs.filter(
  645. in_progress=True, is_paused=False
  646. )
  647. data["listening"] = progress_plays.filter(
  648. Q(track__isnull=False) | Q(podcast_episode__isnull=False)
  649. ).first()
  650. data["watching"] = progress_plays.filter(video__isnull=False).first()
  651. data["going"] = progress_plays.filter(
  652. geo_location__isnull=False
  653. ).first()
  654. data["playing"] = progress_plays.filter(
  655. board_game__isnull=False
  656. ).first()
  657. data["sporting"] = progress_plays.filter(
  658. sport_event__isnull=False
  659. ).first()
  660. data["browsing"] = progress_plays.filter(
  661. web_page__isnull=False
  662. ).first()
  663. data["participating"] = progress_plays.filter(
  664. life_event__isnull=False
  665. ).first()
  666. data["working"] = progress_plays.filter(task__isnull=False).first()
  667. long_plays = user_scrobble_qs.filter(
  668. long_play_complete=False, played_to_completion=True
  669. )
  670. data["reading"] = long_plays.filter(book__isnull=False).first()
  671. data["sessioning"] = long_plays.filter(
  672. video_game__isnull=False
  673. ).first()
  674. return data