scrobblers.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import json
  2. import logging
  3. from typing import Optional
  4. import pendulum
  5. from boardgames.models import BoardGame
  6. from books.models import Book
  7. from dateutil.parser import parse
  8. from django.utils import timezone
  9. from locations.constants import LOCATION_PROVIDERS
  10. from locations.models import GeoLocation
  11. from music.constants import JELLYFIN_POST_KEYS, MOPIDY_POST_KEYS
  12. from music.models import Track
  13. from music.utils import get_or_create_track
  14. from podcasts.utils import get_or_create_podcast
  15. from scrobbles.constants import JELLYFIN_AUDIO_ITEM_TYPES
  16. from scrobbles.models import Scrobble
  17. from scrobbles.utils import convert_to_seconds
  18. from sports.models import SportEvent
  19. from sports.thesportsdb import lookup_event_from_thesportsdb
  20. from videogames.howlongtobeat import lookup_game_from_hltb
  21. from videogames.models import VideoGame
  22. from videos.models import Video
  23. from webpages.models import WebPage
  24. logger = logging.getLogger(__name__)
  25. def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
  26. media_type = Scrobble.MediaType.TRACK
  27. if "podcast" in post_data.get("mopidy_uri", ""):
  28. media_type = Scrobble.MediaType.PODCAST_EPISODE
  29. logger.info(
  30. "[scrobblers] webhook mopidy scrobble request received",
  31. extra={
  32. "user_id": user_id,
  33. "post_data": post_data,
  34. "media_type": media_type,
  35. },
  36. )
  37. if media_type == Scrobble.MediaType.PODCAST_EPISODE:
  38. media_obj = get_or_create_podcast(post_data)
  39. else:
  40. media_obj = get_or_create_track(post_data, MOPIDY_POST_KEYS)
  41. # Now we run off a scrobble
  42. playback_seconds = post_data.get("playback_time_ticks", 1) / 1000
  43. playback_status = post_data.get(MOPIDY_POST_KEYS.get("STATUS"), "")
  44. return media_obj.scrobble_for_user(
  45. user_id,
  46. source="Mopidy",
  47. playback_position_seconds=playback_seconds,
  48. status=playback_status,
  49. )
  50. def jellyfin_scrobble_media(
  51. post_data: dict, user_id: int
  52. ) -> Optional[Scrobble]:
  53. media_type = Scrobble.MediaType.VIDEO
  54. if post_data.pop("ItemType", "") in JELLYFIN_AUDIO_ITEM_TYPES:
  55. media_type = Scrobble.MediaType.TRACK
  56. logger.info(
  57. "[jellyfin_scrobble_track] called",
  58. extra={
  59. "user_id": user_id,
  60. "post_data": post_data,
  61. "media_type": media_type,
  62. },
  63. )
  64. null_position_on_progress = (
  65. post_data.get("PlaybackPosition") == "00:00:00"
  66. and post_data.get("NotificationType") == "PlaybackProgress"
  67. )
  68. playback_status = "resumed"
  69. if post_data.get("IsPaused"):
  70. playback_status = "paused"
  71. elif post_data.get("NotificationType") == "PlaybackStop":
  72. playback_status = "stopped"
  73. # Jellyfin has some race conditions with it's webhooks, these hacks fix some of them
  74. if null_position_on_progress:
  75. logger.info(
  76. "[jellyfin_scrobble_track] no playback position tick, aborting",
  77. extra={"post_data": post_data},
  78. )
  79. return
  80. playback_position_seconds = convert_to_seconds(
  81. post_data.get(JELLYFIN_POST_KEYS.get("RUN_TIME"), 0)
  82. )
  83. if media_type == Scrobble.MediaType.VIDEO:
  84. media_obj = Video.find_or_create(post_data)
  85. else:
  86. media_obj = get_or_create_track(
  87. post_data, post_keys=JELLYFIN_POST_KEYS
  88. )
  89. return media_obj.scrobble_for_user_id(
  90. user_id,
  91. playback_position_seconds=playback_position_seconds,
  92. status=playback_status,
  93. )
  94. def manual_scrobble_video(imdb_id: str, user_id: int):
  95. video = Video.find_or_create({"imdb_id": imdb_id})
  96. # When manually scrobbling, try finding a source from the series
  97. source = "Vrobbler"
  98. if video.tv_series:
  99. source = video.tv_series.preferred_source
  100. scrobble_dict = {
  101. "user_id": user_id,
  102. "timestamp": timezone.now(),
  103. "playback_position_seconds": 0,
  104. "source": source,
  105. }
  106. logger.info(
  107. "[scrobblers] manual video scrobble request received",
  108. extra={
  109. "video_id": video.id,
  110. "user_id": user_id,
  111. "scrobble_dict": scrobble_dict,
  112. "media_type": Scrobble.MediaType.VIDEO,
  113. },
  114. )
  115. return Scrobble.create_or_update(video, user_id, scrobble_dict)
  116. def manual_scrobble_event(thesportsdb_id: str, user_id: int):
  117. data_dict = lookup_event_from_thesportsdb(thesportsdb_id)
  118. event = SportEvent.find_or_create(data_dict)
  119. scrobble_dict = build_scrobble_dict(data_dict, user_id)
  120. return Scrobble.create_or_update(event, user_id, scrobble_dict)
  121. def manual_scrobble_video_game(hltb_id: str, user_id: int):
  122. game = VideoGame.objects.filter(hltb_id=hltb_id).first()
  123. if not game:
  124. data_dict = lookup_game_from_hltb(hltb_id)
  125. game = VideoGame.find_or_create(data_dict)
  126. scrobble_dict = {
  127. "user_id": user_id,
  128. "timestamp": timezone.now(),
  129. "playback_position_seconds": 0,
  130. "source": "Vrobbler",
  131. "long_play_complete": False,
  132. }
  133. logger.info(
  134. "[scrobblers] manual video game scrobble request received",
  135. extra={
  136. "videogame_id": game.id,
  137. "user_id": user_id,
  138. "scrobble_dict": scrobble_dict,
  139. "media_type": Scrobble.MediaType.VIDEO_GAME,
  140. },
  141. )
  142. return Scrobble.create_or_update(game, user_id, scrobble_dict)
  143. def manual_scrobble_book(openlibrary_id: str, user_id: int):
  144. book = Book.find_or_create(openlibrary_id)
  145. scrobble_dict = {
  146. "user_id": user_id,
  147. "timestamp": timezone.now(),
  148. "playback_position_seconds": 0,
  149. "source": "Vrobbler",
  150. "long_play_complete": False,
  151. }
  152. logger.info(
  153. "[scrobblers] manual book scrobble request received",
  154. extra={
  155. "book_id": book.id,
  156. "user_id": user_id,
  157. "scrobble_dict": scrobble_dict,
  158. "media_type": Scrobble.MediaType.BOOK,
  159. },
  160. )
  161. return Scrobble.create_or_update(book, user_id, scrobble_dict)
  162. def manual_scrobble_board_game(bggeek_id: str, user_id: int):
  163. boardgame = BoardGame.find_or_create(bggeek_id)
  164. if not boardgame:
  165. logger.error(f"No board game found for ID {bggeek_id}")
  166. return
  167. scrobble_dict = {
  168. "user_id": user_id,
  169. "timestamp": timezone.now(),
  170. "playback_position_seconds": 0,
  171. "source": "Vrobbler",
  172. }
  173. logger.info(
  174. "[webhook] board game scrobble request received",
  175. extra={
  176. "boardgame_id": boardgame.id,
  177. "user_id": user_id,
  178. "scrobble_dict": scrobble_dict,
  179. "media_type": Scrobble.MediaType.BOARD_GAME,
  180. },
  181. )
  182. return Scrobble.create_or_update(boardgame, user_id, scrobble_dict)
  183. def manual_scrobble_webpage(url: str, user_id: int):
  184. webpage = WebPage.find_or_create({"url": url})
  185. scrobble_dict = {
  186. "user_id": user_id,
  187. "timestamp": timezone.now(),
  188. "playback_position_seconds": 0,
  189. "source": "Vrobbler",
  190. }
  191. logger.info(
  192. "[webhook] webpage scrobble request received",
  193. extra={
  194. "webpage_id": webpage.id,
  195. "user_id": user_id,
  196. "scrobble_dict": scrobble_dict,
  197. "media_type": Scrobble.MediaType.WEBPAGE,
  198. },
  199. )
  200. scrobble = Scrobble.create_or_update(webpage, user_id, scrobble_dict)
  201. # possibly async this?
  202. scrobble.push_to_archivebox()
  203. return scrobble
  204. def gpslogger_scrobble_location(data_dict: dict, user_id: int) -> Scrobble:
  205. location = GeoLocation.find_or_create(data_dict)
  206. timestamp = pendulum.parse(data_dict.get("time", timezone.now()))
  207. extra_data = {
  208. "user_id": user_id,
  209. "timestamp": timestamp,
  210. "source": "GPSLogger",
  211. "media_type": Scrobble.MediaType.GEO_LOCATION,
  212. }
  213. scrobble = Scrobble.create_or_update_location(
  214. location,
  215. extra_data,
  216. user_id,
  217. )
  218. provider = LOCATION_PROVIDERS[data_dict.get("prov")]
  219. if "gps_updates" not in scrobble.log.keys():
  220. scrobble.log["gps_updates"] = []
  221. scrobble.log["gps_updates"].append(
  222. {
  223. "timestamp": data_dict.get("time"),
  224. "position_provider": provider,
  225. }
  226. )
  227. if scrobble.timestamp:
  228. scrobble.playback_position_seconds = (
  229. timezone.now() - scrobble.timestamp
  230. ).seconds
  231. scrobble.save(update_fields=["log", "playback_position_seconds"])
  232. logger.info(
  233. "[webhook] gpslogger scrobble request received",
  234. extra={
  235. "scrobble_id": scrobble.id,
  236. "provider": provider,
  237. "user_id": user_id,
  238. "timestamp": extra_data.get("timestamp"),
  239. "raw_timestamp": data_dict.get("time"),
  240. "media_type": Scrobble.MediaType.GEO_LOCATION,
  241. },
  242. )
  243. return scrobble