views.py 26 KB

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