scrobblers.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import logging
  2. from typing import Optional
  3. import pendulum
  4. from boardgames.bgg import lookup_boardgame_from_bgg
  5. from boardgames.models import BoardGame
  6. from books.models import Book
  7. from books.openlibrary import lookup_book_from_openlibrary
  8. from dateutil.parser import parse
  9. from django.utils import timezone
  10. from locations.constants import LOCATION_PROVIDERS
  11. from locations.models import GeoLocation
  12. from music.constants import JELLYFIN_POST_KEYS
  13. from music.models import Track
  14. from music.utils import (
  15. get_or_create_album,
  16. get_or_create_artist,
  17. get_or_create_track,
  18. )
  19. from podcasts.models import PodcastEpisode
  20. from scrobbles.models import Scrobble
  21. from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
  22. from sports.models import SportEvent
  23. from sports.thesportsdb import lookup_event_from_thesportsdb
  24. from videogames.howlongtobeat import lookup_game_from_hltb
  25. from videogames.models import VideoGame
  26. from videos.models import Video
  27. from webpages.models import WebPage
  28. logger = logging.getLogger(__name__)
  29. def mopidy_scrobble_podcast(
  30. data_dict: dict, user_id: Optional[int]
  31. ) -> Scrobble:
  32. mopidy_uri = data_dict.get("mopidy_uri", "")
  33. parsed_data = parse_mopidy_uri(mopidy_uri)
  34. producer_dict = {"name": data_dict.get("artist")}
  35. podcast_name = data_dict.get("album")
  36. if not podcast_name:
  37. podcast_name = parsed_data.get("podcast_name")
  38. podcast_dict = {"name": podcast_name}
  39. episode_name = parsed_data.get("episode_filename")
  40. episode_dict = {
  41. "title": episode_name,
  42. "run_time_seconds": data_dict.get("run_time"),
  43. "number": parsed_data.get("episode_num"),
  44. "pub_date": parsed_data.get("pub_date"),
  45. "mopidy_uri": mopidy_uri,
  46. }
  47. episode = PodcastEpisode.find_or_create(
  48. podcast_dict, producer_dict, episode_dict
  49. )
  50. # Now we run off a scrobble
  51. mopidy_data = {
  52. "user_id": user_id,
  53. "timestamp": timezone.now(),
  54. "playback_position_seconds": data_dict.get("playback_time_ticks"),
  55. "source": "Mopidy",
  56. "mopidy_status": data_dict.get("status"),
  57. }
  58. scrobble = None
  59. if episode:
  60. scrobble = Scrobble.create_or_update(episode, user_id, mopidy_data)
  61. return scrobble
  62. def mopidy_scrobble_track(
  63. data_dict: dict, user_id: Optional[int]
  64. ) -> Optional[Scrobble]:
  65. artist = get_or_create_artist(
  66. data_dict.get("artist"),
  67. mbid=data_dict.get("musicbrainz_artist_id", None),
  68. )
  69. album = get_or_create_album(
  70. data_dict.get("album"),
  71. artist=artist,
  72. mbid=data_dict.get("musicbrainz_album_id"),
  73. )
  74. track = get_or_create_track(
  75. title=data_dict.get("name"),
  76. mbid=data_dict.get("musicbrainz_track_id"),
  77. artist=artist,
  78. album=album,
  79. run_time_seconds=data_dict.get("run_time"),
  80. )
  81. # Now we run off a scrobble
  82. playback_seconds = data_dict.get("playback_time_ticks") / 1000
  83. mopidy_data = {
  84. "user_id": user_id,
  85. "timestamp": timezone.now(),
  86. "playback_position_seconds": playback_seconds,
  87. "source": "Mopidy",
  88. "mopidy_status": data_dict.get("status"),
  89. }
  90. scrobble = Scrobble.create_or_update(track, user_id, mopidy_data)
  91. return scrobble
  92. def build_scrobble_dict(data_dict: dict, user_id: int) -> dict:
  93. jellyfin_status = "resumed"
  94. if data_dict.get("IsPaused"):
  95. jellyfin_status = "paused"
  96. elif data_dict.get("NotificationType") == "PlaybackStop":
  97. jellyfin_status = "stopped"
  98. playback_seconds = convert_to_seconds(
  99. data_dict.get("PlaybackPosition", "")
  100. )
  101. return {
  102. "user_id": user_id,
  103. "timestamp": parse(data_dict.get("UtcTimestamp")),
  104. "playback_position_seconds": playback_seconds,
  105. "source": data_dict.get("ClientName", "Vrobbler"),
  106. "source_id": data_dict.get("MediaSourceId"),
  107. "jellyfin_status": jellyfin_status,
  108. }
  109. def jellyfin_scrobble_track(
  110. data_dict: dict, user_id: Optional[int]
  111. ) -> Optional[Scrobble]:
  112. null_position_on_progress = (
  113. data_dict.get("PlaybackPosition") == "00:00:00"
  114. and data_dict.get("NotificationType") == "PlaybackProgress"
  115. )
  116. # Jellyfin has some race conditions with it's webhooks, these hacks fix some of them
  117. if null_position_on_progress:
  118. logger.error("No playback position tick from Jellyfin, aborting")
  119. return
  120. artist = get_or_create_artist(
  121. data_dict.get(JELLYFIN_POST_KEYS["ARTIST_NAME"]),
  122. mbid=data_dict.get(JELLYFIN_POST_KEYS["ARTIST_MB_ID"]),
  123. )
  124. album = get_or_create_album(
  125. data_dict.get(JELLYFIN_POST_KEYS["ALBUM_NAME"]),
  126. artist=artist,
  127. mbid=data_dict.get(JELLYFIN_POST_KEYS["ALBUM_MB_ID"]),
  128. )
  129. run_time = convert_to_seconds(
  130. data_dict.get(JELLYFIN_POST_KEYS["RUN_TIME"])
  131. )
  132. track = get_or_create_track(
  133. title=data_dict.get("Name"),
  134. artist=artist,
  135. album=album,
  136. run_time_seconds=run_time,
  137. )
  138. scrobble_dict = build_scrobble_dict(data_dict, user_id)
  139. # A hack to make Jellyfin work more like Mopidy for music tracks
  140. scrobble_dict["playback_position_seconds"] = 0
  141. return Scrobble.create_or_update(track, user_id, scrobble_dict)
  142. def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
  143. video = Video.find_or_create(data_dict)
  144. scrobble_dict = build_scrobble_dict(data_dict, user_id)
  145. return Scrobble.create_or_update(video, user_id, scrobble_dict)
  146. def manual_scrobble_video(imdb_id: str, user_id: int):
  147. video = Video.find_or_create({"imdb_id": imdb_id})
  148. # When manually scrobbling, try finding a source from the series
  149. source = "Vrobbler"
  150. if video.tv_series:
  151. source = video.tv_series.preferred_source
  152. scrobble_dict = {
  153. "user_id": user_id,
  154. "timestamp": timezone.now(),
  155. "playback_position_seconds": 0,
  156. "source": source,
  157. "source_id": "Manually scrobbled from Vrobbler and looked up via IMDB",
  158. }
  159. return Scrobble.create_or_update(video, user_id, scrobble_dict)
  160. def manual_scrobble_event(thesportsdb_id: str, user_id: int):
  161. data_dict = lookup_event_from_thesportsdb(thesportsdb_id)
  162. event = SportEvent.find_or_create(data_dict)
  163. scrobble_dict = build_scrobble_dict(data_dict, user_id)
  164. return Scrobble.create_or_update(event, user_id, scrobble_dict)
  165. def manual_scrobble_video_game(hltb_id: str, user_id: int):
  166. game = VideoGame.objects.filter(hltb_id=hltb_id).first()
  167. if not game:
  168. data_dict = lookup_game_from_hltb(hltb_id)
  169. game = VideoGame.find_or_create(data_dict)
  170. scrobble_dict = {
  171. "user_id": user_id,
  172. "timestamp": timezone.now(),
  173. "playback_position_seconds": 0,
  174. "source": "Vrobbler",
  175. "source_id": "Manually scrobbled from Vrobbler and looked up via HLTB.com",
  176. "long_play_complete": False,
  177. }
  178. return Scrobble.create_or_update(game, user_id, scrobble_dict)
  179. def manual_scrobble_book(openlibrary_id: str, user_id: int):
  180. book = Book.find_or_create(openlibrary_id)
  181. scrobble_dict = {
  182. "user_id": user_id,
  183. "timestamp": timezone.now(),
  184. "playback_position_seconds": 0,
  185. "source": "Vrobbler",
  186. "long_play_complete": False,
  187. }
  188. return Scrobble.create_or_update(book, user_id, scrobble_dict)
  189. def manual_scrobble_board_game(bggeek_id: str, user_id: int):
  190. boardgame = BoardGame.find_or_create(bggeek_id)
  191. if not boardgame:
  192. logger.error(f"No board game found for ID {bggeek_id}")
  193. return
  194. scrobble_dict = {
  195. "user_id": user_id,
  196. "timestamp": timezone.now(),
  197. "playback_position_seconds": 0,
  198. "source": "Vrobbler",
  199. "source_id": "Manually scrobbled from Vrobbler and looked up via boardgamegeek.com",
  200. }
  201. return Scrobble.create_or_update(boardgame, user_id, scrobble_dict)
  202. def manual_scrobble_webpage(url: str, user_id: int):
  203. webpage = WebPage.find_or_create({"url": url})
  204. scrobble_dict = {
  205. "user_id": user_id,
  206. "timestamp": timezone.now(),
  207. "playback_position_seconds": 0,
  208. "source": "Vrobbler",
  209. "source_id": "Manually scrobbled from Vrobbler",
  210. }
  211. return Scrobble.create_or_update(webpage, user_id, scrobble_dict)
  212. def gpslogger_scrobble_location(
  213. data_dict: dict, user_id: Optional[int]
  214. ) -> Optional[Scrobble]:
  215. # Save the data coming in
  216. if not user_id:
  217. user_id = 1 # TODO fix authing the end point to get user
  218. location = GeoLocation.find_or_create(data_dict)
  219. # Now we run off a scrobble
  220. playback_seconds = 1
  221. extra_data = {
  222. "user_id": user_id,
  223. "timestamp": pendulum.parse(data_dict.get("time", timezone.now())),
  224. "playback_position_seconds": playback_seconds,
  225. "source": "GPSLogger",
  226. }
  227. scrobble = Scrobble.create_or_update(location, user_id, extra_data)
  228. provider = f"data source: {LOCATION_PROVIDERS[data_dict.get('prov')]}"
  229. if scrobble:
  230. if scrobble.notes:
  231. scrobble.notes = scrobble.notes + f"\n{provider}"
  232. else:
  233. scrobble.notes = provider
  234. scrobble.save(update_fields=["notes"])
  235. return scrobble