models.py 5.2 KB

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