models.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import logging
  2. from typing import Dict, Optional
  3. from uuid import uuid4
  4. import musicbrainzngs
  5. from django.apps.config import cached_property
  6. from django.conf import settings
  7. from django.core.files.base import ContentFile
  8. from django.db import models
  9. from django.utils.translation import gettext_lazy as _
  10. from django_extensions.db.models import TimeStampedModel
  11. from scrobbles.mixins import ScrobblableMixin
  12. logger = logging.getLogger(__name__)
  13. BNULL = {"blank": True, "null": True}
  14. class Artist(TimeStampedModel):
  15. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  16. name = models.CharField(max_length=255)
  17. musicbrainz_id = models.CharField(max_length=255, **BNULL)
  18. class Meta:
  19. unique_together = [['name', 'musicbrainz_id']]
  20. def __str__(self):
  21. return self.name
  22. @property
  23. def mb_link(self):
  24. return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
  25. class Album(TimeStampedModel):
  26. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  27. name = models.CharField(max_length=255)
  28. artists = models.ManyToManyField(Artist)
  29. year = models.IntegerField(**BNULL)
  30. musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
  31. musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
  32. musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
  33. cover_image = models.ImageField(upload_to="albums/", **BNULL)
  34. def __str__(self):
  35. return self.name
  36. @property
  37. def primary_artist(self):
  38. return self.artists.first()
  39. def fix_metadata(self):
  40. if not self.musicbrainz_albumartist_id or not self.year:
  41. musicbrainzngs.set_useragent('vrobbler', '0.3.0')
  42. mb_data = musicbrainzngs.get_release_by_id(
  43. self.musicbrainz_id, includes=['artists']
  44. )
  45. if not self.musicbrainz_albumartist_id:
  46. self.musicbrainz_albumartist_id = mb_data['release'][
  47. 'artist-credit'
  48. ][0]['artist']['id']
  49. if not self.year:
  50. try:
  51. self.year = mb_data['release']['date'][0:4]
  52. except KeyError:
  53. pass
  54. except IndexError:
  55. pass
  56. self.save(update_fields=['musicbrainz_albumartist_id', 'year'])
  57. new_artist = Artist.objects.filter(
  58. musicbrainz_id=self.musicbrainz_albumartist_id
  59. ).first()
  60. if self.musicbrainz_albumartist_id and new_artist:
  61. self.artists.add(new_artist)
  62. if not new_artist:
  63. for t in self.track_set.all():
  64. self.artists.add(t.artist)
  65. def fetch_artwork(self):
  66. if not self.cover_image:
  67. try:
  68. img_data = musicbrainzngs.get_image_front(self.musicbrainz_id)
  69. name = f"{self.name}_{self.uuid}.jpg"
  70. self.cover_image = ContentFile(img_data, name=name)
  71. except musicbrainzngs.ResponseError:
  72. logger.warning(f'No cover art found for {self.name}')
  73. self.cover_image = 'default-image-replace-me'
  74. self.save()
  75. @property
  76. def mb_link(self):
  77. return f"https://musicbrainz.org/release/{self.musicbrainz_id}"
  78. class Track(ScrobblableMixin):
  79. RESUME_LIMIT = getattr(settings, 'MUSIC_RESUME_LIMIT', 60 * 60)
  80. COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)
  81. class Opinion(models.IntegerChoices):
  82. DOWN = -1, 'Thumbs down'
  83. NEUTRAL = 0, 'No opinion'
  84. UP = 1, 'Thumbs up'
  85. artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
  86. album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
  87. musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
  88. def __str__(self):
  89. return f"{self.title} by {self.artist}"
  90. @property
  91. def mb_link(self):
  92. return f"https://musicbrainz.org/recording/{self.musicbrainz_id}"
  93. @cached_property
  94. def scrobble_count(self):
  95. return self.scrobble_set.filter(in_progress=False).count()
  96. @classmethod
  97. def find_or_create(
  98. cls, artist_dict: Dict, album_dict: Dict, track_dict: Dict
  99. ) -> Optional["Track"]:
  100. """Given a data dict from Jellyfin, does the heavy lifting of looking up
  101. the video and, if need, TV Series, creating both if they don't yet
  102. exist.
  103. """
  104. if not artist_dict.get('name') or not artist_dict.get(
  105. 'musicbrainz_id'
  106. ):
  107. logger.warning(
  108. f"No artist or artist musicbrainz ID found in message from source, not scrobbling"
  109. )
  110. return
  111. artist, artist_created = Artist.objects.get_or_create(**artist_dict)
  112. if artist_created:
  113. logger.debug(f"Created new artist {artist}")
  114. else:
  115. logger.debug(f"Found album {artist}")
  116. album, album_created = Album.objects.get_or_create(**album_dict)
  117. if album_created:
  118. logger.debug(f"Created new album {album}")
  119. else:
  120. logger.debug(f"Found album {album}")
  121. album.fix_metadata()
  122. if not album.cover_image:
  123. album.fetch_artwork()
  124. track_dict['album_id'] = getattr(album, "id", None)
  125. track_dict['artist_id'] = artist.id
  126. track, created = cls.objects.get_or_create(**track_dict)
  127. if created:
  128. logger.debug(f"Created new track: {track}")
  129. else:
  130. logger.debug(f"Found track {track}")
  131. return track