scrobblers.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import logging
  2. from typing import Optional
  3. from dateutil.parser import parse
  4. from django.utils import timezone
  5. from music.constants import JELLYFIN_POST_KEYS
  6. from music.models import Track
  7. from podcasts.models import Episode
  8. from scrobbles.models import Scrobble
  9. from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
  10. from videos.models import Video
  11. from sports.models import SportEvent
  12. logger = logging.getLogger(__name__)
  13. def mopidy_scrobble_podcast(
  14. data_dict: dict, user_id: Optional[int]
  15. ) -> Scrobble:
  16. mopidy_uri = data_dict.get("mopidy_uri", "")
  17. parsed_data = parse_mopidy_uri(mopidy_uri)
  18. producer_dict = {"name": data_dict.get("artist")}
  19. podcast_name = data_dict.get("album")
  20. if not podcast_name:
  21. podcast_name = parsed_data.get("podcast_name")
  22. podcast_dict = {"name": podcast_name}
  23. episode_name = parsed_data.get("episode_filename")
  24. episode_dict = {
  25. "title": episode_name,
  26. "run_time_ticks": data_dict.get("run_time_ticks"),
  27. "run_time": data_dict.get("run_time"),
  28. "number": parsed_data.get("episode_num"),
  29. "pub_date": parsed_data.get("pub_date"),
  30. "mopidy_uri": mopidy_uri,
  31. }
  32. episode = Episode.find_or_create(podcast_dict, producer_dict, episode_dict)
  33. # Now we run off a scrobble
  34. mopidy_data = {
  35. "user_id": user_id,
  36. "timestamp": timezone.now(),
  37. "playback_position_ticks": data_dict.get("playback_time_ticks"),
  38. "source": "Mopidy",
  39. "mopidy_status": data_dict.get("status"),
  40. }
  41. scrobble = None
  42. if episode:
  43. scrobble = Scrobble.create_or_update_for_podcast_episode(
  44. episode, user_id, mopidy_data
  45. )
  46. return scrobble
  47. def mopidy_scrobble_track(
  48. data_dict: dict, user_id: Optional[int]
  49. ) -> Optional[Scrobble]:
  50. artist_dict = {
  51. "name": data_dict.get("artist", None),
  52. "musicbrainz_id": data_dict.get("musicbrainz_artist_id", None),
  53. }
  54. album_dict = {
  55. "name": data_dict.get("album"),
  56. "musicbrainz_id": data_dict.get("musicbrainz_album_id"),
  57. }
  58. track_dict = {
  59. "title": data_dict.get("name"),
  60. "run_time_ticks": data_dict.get("run_time_ticks"),
  61. "run_time": data_dict.get("run_time"),
  62. }
  63. track = Track.find_or_create(artist_dict, album_dict, track_dict)
  64. # Now we run off a scrobble
  65. mopidy_data = {
  66. "user_id": user_id,
  67. "timestamp": timezone.now(),
  68. "playback_position_ticks": data_dict.get("playback_time_ticks"),
  69. "source": "Mopidy",
  70. "mopidy_status": data_dict.get("status"),
  71. }
  72. # Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
  73. track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
  74. track.save()
  75. scrobble = Scrobble.create_or_update_for_track(track, user_id, mopidy_data)
  76. return scrobble
  77. def create_jellyfin_scrobble_dict(data_dict: dict, user_id: int) -> dict:
  78. jellyfin_status = "resumed"
  79. if data_dict.get("IsPaused"):
  80. jellyfin_status = "paused"
  81. if data_dict.get("PlayedToCompletion"):
  82. jellyfin_status = "stopped"
  83. playback_position_ticks = None
  84. if data_dict.get("PlaybackPositionTicks"):
  85. playback_position_ticks = (
  86. data_dict.get("PlaybackPositionTicks") // 10000
  87. )
  88. if playback_position_ticks <= 0:
  89. playback_position_ticks = None
  90. playback_position = data_dict.get("PlaybackPosition")
  91. if playback_position:
  92. playback_position = convert_to_seconds(playback_position)
  93. return {
  94. "user_id": user_id,
  95. "timestamp": parse(data_dict.get("UtcTimestamp")),
  96. "playback_position_ticks": playback_position_ticks,
  97. "playback_position": playback_position,
  98. "source": "Jellyfin",
  99. "source_id": data_dict.get('MediaSourceId'),
  100. "jellyfin_status": jellyfin_status,
  101. }
  102. def jellyfin_scrobble_track(
  103. data_dict: dict, user_id: Optional[int]
  104. ) -> Optional[Scrobble]:
  105. if not data_dict.get("Provider_musicbrainztrack", None):
  106. logger.error(
  107. "No MBrainz Track ID received. This is likely because all metadata is bad, not scrobbling"
  108. )
  109. return
  110. artist_dict = {
  111. 'name': data_dict.get(JELLYFIN_POST_KEYS["ARTIST_NAME"], None),
  112. 'musicbrainz_id': data_dict.get(
  113. JELLYFIN_POST_KEYS["ARTIST_MB_ID"], None
  114. ),
  115. }
  116. album_dict = {
  117. "name": data_dict.get(JELLYFIN_POST_KEYS["ALBUM_NAME"], None),
  118. "musicbrainz_id": data_dict.get(JELLYFIN_POST_KEYS['ALBUM_MB_ID']),
  119. }
  120. # Convert ticks from Jellyfin from microseconds to nanoseconds
  121. # Ain't nobody got time for nanoseconds
  122. track_dict = {
  123. "title": data_dict.get("Name", ""),
  124. "run_time_ticks": data_dict.get(
  125. JELLYFIN_POST_KEYS["RUN_TIME_TICKS"], None
  126. )
  127. // 10000,
  128. "run_time": convert_to_seconds(
  129. data_dict.get(JELLYFIN_POST_KEYS["RUN_TIME"], None)
  130. ),
  131. }
  132. track = Track.find_or_create(artist_dict, album_dict, track_dict)
  133. # Prefer Mopidy MD IDs to Jellyfin, so skip if we already have one
  134. if not track.musicbrainz_id:
  135. track.musicbrainz_id = data_dict.get(
  136. JELLYFIN_POST_KEYS["TRACK_MB_ID"], None
  137. )
  138. track.save()
  139. scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
  140. return Scrobble.create_or_update_for_track(track, user_id, scrobble_dict)
  141. def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
  142. if not data_dict.get("Provider_imdb", None):
  143. logger.error(
  144. "No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
  145. )
  146. return
  147. video = Video.find_or_create(data_dict)
  148. scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
  149. return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
  150. def manual_scrobble_video(data_dict: dict, user_id: Optional[int]):
  151. if not data_dict.get("Provider_imdb", None):
  152. logger.error(
  153. "No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
  154. )
  155. return
  156. video = Video.find_or_create(data_dict)
  157. scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
  158. return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
  159. def manual_scrobble_event(data_dict: dict, user_id: Optional[int]):
  160. if not data_dict.get("Provider_thesportsdb", None):
  161. logger.error(
  162. "No TheSportsDB ID received. This is likely because all metadata is bad, not scrobbling"
  163. )
  164. return
  165. event = SportEvent.find_or_create(data_dict)
  166. scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
  167. return Scrobble.create_or_update_for_sport_event(
  168. event, user_id, scrobble_dict
  169. )